Skip to main content
Docs navigation

Author your own pack

A pack is a directory of YAML you write and maintain yourself — there is nothing to compile or upload. Put it on the runners you control, trust it once, and your model can call it. Most packs stay private to your fleet; the public registry is curated (more below).

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

$ sudo emisar pack install ./my-pack
installed my-pack → /etc/emisar/packs/my-pack

my-pack — My ops pack  (v0.1.0)
  Short one-line summary shown on the runner + console.

  Actions:   2  (2 low)
  Requires:  linux · my-cli ✓
  Hash:      sha256:e8edc3660cb2cf7a8f3fe0d9cea167d07565b8287f57bd089b4dd737eb0e3844
  Docs:      https://github.acme.internal/sre/my-pack



  Verify
    ok my.do_other_thing  5ms

Reloaded the runner — it re-reads packs and re-advertises to the control plane.
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. Lay out the pack directory#

A pack is a folder with a top-level pack.yaml manifest and one YAML file per action in actions/. An optional scripts/ directory holds packaged shell scripts for kind: script actions. Keep the pack in your own Git repository.

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. Write pack.yaml#

The manifest declares pack metadata and the action YAML files to load. id is the pack slug. It appears in the install path and your internal references.

yaml
schema_version: 1
id: my-pack
name: My ops pack
version: 0.1.0
description: Short one-line summary shown on the runner + console.
vendor: acme
homepage: https://github.acme.internal/sre/my-pack   # your repo; optional

requires:
  os: [linux]
  binaries: [journalctl]  # optional — a missing one fails at run time

setup:                    # optional — powers pack info, verify, and the install probe
  summary: >
    Reads the local system journal.
  host_access:
    - actions: [my.journal_tail]
      requirement: Read the system journal.
      recipes:
        - name: systemd Linux — default emisar service user
          commands:
            - sudo usermod -aG systemd-journal emisar
            - sudo systemctl restart emisar
          verify:
            - sudo -u emisar journalctl -n 1 --no-pager
          impact: The emisar service identity can read the complete system journal.
  verify: my.journal_tail  # a no-argument, low-risk connectivity check

actions:
  - actions/journal_tail.yaml

The setup block is what emisar pack info prints and what pack verify and the install probe run. Declare the env your tool reads, map each protected host resource to the exact actions that need it, and name one low-risk read as verify. Host-access recipes are instructions for the operator; Emisar displays and copies their commands but never runs them.

3. Declare each action#

Create one YAML file for each action. The runner refuses anything outside the catalog. Each file defines the contract between your operations team and the LLM. Safety depends on tight argument validation, an accurate side_effects list, and the correct risk tier. See the Pack reference for all action fields.

yaml
schema_version: 1
id: my.journal_tail
title: Read recent system journal entries
kind: exec
risk: low

description: >
  Show the most recent system journal entries.

side_effects:
  - Reads the system journal.
  - Touches nothing.

args:
  - name: lines
    type: integer
    default: 100
    validation:
      min: 1
      max: 1000

execution:
  command:
    binary: journalctl       # bare name — resolved via PATH on the host
    argv: ["--no-pager", "-n", "{{ args.lines }}"]
  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: Read the last 100 entries
    args:
      lines: 100

4. Validate, install, and trust it#

Validate the pack locally — it runs the same checks as the runner — then install it. The install writes to the configured pack directory, reloads the runner without a restart or dropped runs, and ends by probing your setup.verify action, as the output at the top of this page shows:

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

You can validate anywhere: pack validate reads only the directory you point it at, and it does not need a config, a service, or an account. On a laptop — Linux or macOS — or in CI, install just the binary:

shell
# binary-only install — skips the service, needs no enrollment key
$ curl -fsSL https://emisar.dev/install.sh | sudo bash -s -- --no-service
$ emisar pack validate ./my-pack

# in CI, pin the hash you will trust
$ emisar pack validate ./my-pack --json | jq -r .hash

Your pack is not in the published catalog, so it is not trusted automatically: it shows as pending on the console's Packs page, and dispatch for it is refused until you review the content hash there and click Trust. Its actions are then callable, gated by your policy, and that exact version is the only one authorized. Edit the pack and its hash changes, re-marking it pending until you re-trust — so what runs is always the files you reviewed.

Then test it end to end: open the runner from Runners, check your actions under Advertised actions, and Run one.

5. Roll it out and maintain it#

To put the pack on another runner, install it pinned to the hash you trusted — every host then runs the exact same files, and the install reloads the runner for you. Config management — Ansible, Chef, 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, increase its version. Validate and install it again. Add --force to replace an installed pack. Trust the new hash once in Packs. Give it the same review as any production change.

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

Keep it private, or propose it#

We deliberately curate the public registry behind the Packs page . We accept only generic packs that many teams can use unchanged, such as Postgres, Cassandra, Docker, Linux core, and AWS packs. We reject one-off, environment-specific packs: they are hard to trust and maintain, and they bury the packs everyone can use.

Most packs must stay yours. A pack that hardcodes your service names, your hostnames, or your internal CLIs is more useful as a private pack. You also keep control of its lifecycle. Self-maintained is the default.

A private pack can still serve a whole fleet — packctl builds a registry you host yourself, and your packs never enter ours. Host your own registry walks it end to end.

If your pack is useful to many teams and has no environment-specific assumptions, publish it in its own repository and open an issue suggesting we include it. We review for:

  • Genuinely generic. Do not hardcode hosts, service names, or organization-specific paths.
  • Tight argument validation. Reject free-form shell strings. Use allowed_prefixes for paths, validation.enum for choices, and min/max for numbers.
  • Honest side_effects. List every file, network, and process side effect. The LLM reads this list.
  • Correct risk level. Assign high or critical to destructive actions. Assign low to reads.
  • Resolvable binaries. Declare required host binaries under requires.binaries.

Last reviewed September 3, 2026