Skip to main content
Docs navigation

Author your own pack

A pack is a directory of YAML you write and maintain yourself — no SDK, no compiler, no signing keys, nothing uploaded to anyone. Drop it on the runners you control, trust it once, and your model can call it. Most packs stay private to your fleet, and that's by design: the public registry is curated, not a free-for-all (more on that below).

your host
$ emisar pack validate ./my-pack
pack my-pack OK: 2 actions
hash: sha256:e0487aa2762564261db5fc8e7efd5bfef895b33c8d9fa6342b1408cc602a97ab

$ sudo emisar pack install ./my-pack
installed my-pack → /etc/emisar/packs/my-pack
my-pack — My ops pack  (v0.1.0)
  Actions:   2  (2 low)
Use your coding agent

The author-pack skill walks your agent through writing, validating, testing, and trusting a pack of your own.

paste into your agent
Write an emisar action pack for the tool I name, validate and test it, and get it trusted on my runner. Use the author-pack skill: https://raw.githubusercontent.com/AndrewDryga/emisar/main/skills/author-pack/SKILL.md

Works in Claude Code, Codex, or any agent that reads Markdown skills — read the skill first .

  1. 1

    Lay out the pack directory

    A pack is just a folder: a top-level pack.yaml manifest and one YAML per action in actions/. Optional scripts/ holds packaged shell scripts for kind: script actions. Keep the whole thing in your own git repo.

    my-pack/
      pack.yaml                    # pack manifest (see step 2)
      actions/
        do_thing.yaml              # one declared action per file
        do_other_thing.yaml
      scripts/                     # only when you have script actions
        do_thing.sh
  2. 2

    Write pack.yaml

    The manifest declares pack-level metadata and which action YAMLs to load. id is the pack slug (used in the install path and, for self-maintained packs, however you reference it internally).

    yaml
    schema_version: 1
    id: my-pack
    name: My ops pack
    version: 0.1.0
    description: Short one-line summary shown on the runner + dashboard.
    vendor: acme
    homepage: https://github.acme.internal/sre/my-pack   # your repo; optional
    
    requires:
      os: [linux]
      binaries: [my-cli]      # optional — advertised to cloud; a missing one fails at run time
    
    actions:
      - actions/do_thing.yaml
      - actions/do_other_thing.yaml
  3. 3

    Declare each action

    One YAML per action — the runner refuses anything not in the catalog, so this file is the contract between your ops team and the LLM. This is where the safety lives: tight argument validation, an honest side_effects list, and the right risk tier. See the YAML reference for the action fields.

    yaml
    schema_version: 1
    id: my.do_thing
    title: Do the thing
    kind: exec
    risk: low
    
    description: >
      One-paragraph human description. Shown verbatim to the LLM —
      write it like a doc string, not a comment.
    
    side_effects:
      - Reads /var/log/myapp/*.
      - Touches nothing.
    
    args:
      - name: file
        type: path
        required: true
        validation:
          allowed_prefixes: ["/var/log/myapp/"]
    
    execution:
      command:
        binary: cat              # bare name — resolved via PATH on the host
        argv: ["{{ args.file }}"]
      timeout: 10s
    
    output:
      parser: text
      max_stdout_bytes: 65536
      max_stderr_bytes: 8192
    
    # Optional. Renders on the dispatch form so operators (and LLMs)
    # see a real invocation before they fill in args.
    examples:
      - title: Tail myapp's error log
        args:
          file: /var/log/myapp/error.log
  4. 4

    Validate, install, and trust it

    Validate locally first — the same checks the runner runs at load — then install it. Install writes to the runner's configured packs dir and reloads the running daemon itself, so there's no restart and no dropped runs:

    shell
    # on the runner host
    emisar pack validate ./my-pack
    sudo emisar pack install ./my-pack

    Because your pack isn't one emisar publishes, it has no baseline hash — so it lands in the dashboard's Packs page as pending, with dispatch held until you approve it. Open Packs, review the content hash, and click Trust. Its actions are then callable, gated by your policy. From then on, that exact byte-for-byte version is the only one authorized: edit the pack and its hash changes, which re-marks it pending until you re-trust. That's your drift guard — trust in bytes you reviewed, not a publisher you hope is honest.

    Confirm it end-to-end: open the runner in the dashboard, check your actions under Advertised actions, and Run one.

  5. 5

    Roll it out and maintain it

    To put the pack on another runner, install it pinned to the hash you trusted, so every host runs identical bytes — the install reloads the runner for you. (Config management like Ansible, Chef, or a base image can drop the directory itself instead, then reload the runner.)

    shell
    sudo emisar pack install ./my-pack --hash sha256:<the hash you trusted>

    When you change the pack, bump its version, re-validate, re-install (add --force — an already-installed pack isn't replaced without it), and re-trust the new hash once in the dashboard — the same review you'd give any production change. The pack is yours: its lifecycle, its hosts, its risk tiers, on your schedule.

    Distributing many packs to a large fleet? Skip the copying and host your own pack registry.

Keep it yours — or propose it to the registry#

The public registry behind the Packs page is curated by us, deliberately. We only take packs that are genuinely generic and broadly useful — Postgres, Cassandra, Docker, Linux core, the AWS suite — the kind of thing many teams run unchanged. We don't accept every submission, because an open dumping ground of one-off, environment-specific packs would be a mess: hard to trust, hard to maintain, and low signal for everyone browsing it.

Most packs should stay yours. A pack that hardcodes your service names, your hostnames, or your internal CLIs is more useful private — and you keep control of its lifecycle. Self-maintained is the default here, not a fallback.

And keeping a pack private still scales to a whole fleet — no copying files host to host. packctl, the same tool that builds emisar's own registry, publishes yours from a packs/ directory to any bucket or web server you control, and every runner installs from it with emisar pack install <pack> --registry <your-url>. Same hash-pinned review-and-trust flow as the public registry — your packs never touch ours. Host your own pack registry walks it end to end.

If your pack really is generic — useful to teams who've never heard of yours, with no environment-specific assumptions — we'd love to ship it. Open a PR against the repo adding it under packs/ — the catalog is built from the tree, so there's no registry-code entry to edit — or email support@emisar.dev first if you're not sure it clears the bar. We review for:

  • Genuinely generic — no hardcoded hosts, service names, or org-specific paths. If it only makes sense inside your company, it belongs in your repo, not ours.
  • Tight argument validation — no free-form shell strings; paths use allowed_prefixes, choice args use validation.enum, and numbers use min/max.
  • Honest side_effects — every file, network, and process side effect listed. The LLM reads these.
  • Correct risk level — destructive actions (restart, delete, repair) are high or critical; reads are low.
  • Resolvable binaries — declare what the runner needs on the host under requires.binaries.