agentStack

These pages describe v0.19.0, the current release. That is the build the installer gives you. agentstack --version says which build you have.

Automation contract

Everything AgentStack can tell you, it can tell a program. This page is the single list: which commands emit JSON, what each body looks like, and which contract name to check before you depend on one.

If you are driving the CLI from an agent, a script, or a graphical companion such as t3code, this is the page to read. If you want the human-facing flags, see Every command.

The envelope#

Every machine-readable read wraps its body in the same two fields:

json
{
  "schema_version": 1,
  "features": ["status-v1", "diff-v1", "json-reads-v1", "…"],
  "…": "the body's own fields, alongside these two"
}

The two envelope fields are injected into the body object, so a body key and an envelope key live at the same level. Do not name a body key schema_version or features.

Three rules the envelope lets you rely on:

  1. Check the name, not the field. "advisories" in body tells you the binary emitted advisories this time; features.includes("doctor-advisories-v1") tells you it can. Only the second is a contract.
  2. A name never changes meaning. Widening a contract gets a new name — that is why toolset-create-v2 exists next to profiles-edit-v1, and why the four reads below got json-reads-v1 rather than being folded into an older name.
  3. Negotiation is presentation only. No enforcement decision reads these fields. The CLI re-validates every precondition on every call whether or not you negotiated, so a UI that displays the wrong thing cannot cause the wrong thing to happen.

Errors#

A refusal is not a JSON body. When a command cannot answer, stdout stays empty, the reason goes to stderr as human text, and the exit code is non-zero. Parse stdout only when the exit code is 0.

console
$ agentstack use --list --json --manifest-dir /var/empty
error: no agentstack manifest in /var/empty
(a project keeps one at .agentstack/agentstack.toml, or agentstack.toml at the repo root)

$ echo $?
1

The refusal a headless run meets first: trust#

The reads on this page answer for a project in any trust state: status --json and use --list --json report trust: "untrusted" and still exit 0. (The one exception is the read that spawns — doctor --probe refuses to start anything for a project that is not trusted at its current bytes.) Writes are different. For an untrusted or drifted project the trust gate covers five kinds, and each one refuses in place, names the item, and points at the same command:

A headless call toRefuses to
agentstack apply --writewrite native MCP server config, compile instruction fragments, render hooks, render native extensions
agentstack use --writematerialize skills (and the server half above)

Every refusal line has the same shape, naming the item it withheld in parentheses:

refusing to …: project at <path> is not trusted — review and agentstack trust . before …

Trust is recorded per project directory and never copied, so a clone, a second checkout, or a fresh worktree of a project someone already approved is untrusted at its new path. An unattended pipeline against one does not silently render a partial setup: it stops, and the fix is a human review. Budget for it — clone, agentstack more install --locked, then the digest-bound trust --previewtrust --yes --consented pair on this page, before any --write. (agentstack trust . is the interactive form: it asks one closing question, so it exits nonzero with no terminal to answer it.)

Gate on the exit code, and know what it counts. The exit code counts blocked writes, not printed refusals: a blocked pending write exits 1 (error: 1 blocked write on 1 target), while a project whose managed region already matches prints the same line and exits 0, because nothing needed writing. A pipeline that wants "did the gate refuse anything?" rather than "was anything left unwritten?" must read the output, not only $?.

Pruning, machine-layer content and the machine manifest are outside this gate.

Reads#

Every command below is read-only: it does not write a file, render a config, resolve a ${REF} into a value, or start a server — with two deliberate, named exceptions noted in their rows (doctor --probe starts each stdio server and stops it again; lease status runs /bin/ps on macOS to read a process start time).

CommandContract nameBody
agentstack status --jsonjson-reads-v1version, clis_detected, manifest, project, next_action
agentstack search <q> --jsonjson-reads-v1query, results[]
agentstack more adapters list --jsonjson-reads-v1adapters[]
agentstack more session list --jsonjson-reads-v1sessions[]
agentstack doctor --jsonstatus-v1state, next_action, sections, errors, warnings, trust, protection
agentstack doctor --jsondoctor-advisories-v1top-level advisories count; section lines may carry level: "advisory"
agentstack doctor --jsondoctor-mode-v1top-level mode (static / clean-at-rest / zero-files) and activation (locked / never_activated) — the same derived readings status prints, so no prose-matching. activation answers "was this project ever activated", i.e. does a lockfile exist; it is not a liveness reading
agentstack doctor --jsondoctor-liveness-v1top-level live_state (live / not_live), locked, default_toolset, live_toolsets[] — whether the lease registry holds a live record for this project right now. Additive: activation keeps its doctor-mode-v1 values, so gate on this name for the runtime reading
agentstack doctor --jsondoctor-cli-coverage-v1per-CLI coverage — which detected CLIs the current delivery mode actually configures
agentstack status --json / doctor --jsonstatus-honesty-v1state never reports ready over unverified coverage — gate on this name before trusting state: "ready"
agentstack doctor --probe --jsondoctor-probe-v1top-level probe object. This one spawns: it starts each stdio server, speaks the MCP initialize handshake, and stops it again
agentstack use --list --jsonprofiles-v1path, trust, profiles[] with readiness
agentstack use --list --jsonsessions-v1per-entry active, plus the top-level session object
agentstack more diff --jsondiff-v1targets[], drifted, kept, owner_refreshes, scope, warnings
agentstack more diff --jsondiff-ownership-v1per-target managed, hand_edited, foreign_untracked
agentstack more diff --jsondiff-existence-v1per-target existed_before — splits "never rendered here / file absent" from "the manifest moved ahead of a rendered file"
agentstack more restore --jsonrestore-lastentries (newest first) and adapter_backups
agentstack undo --jsonjson-reads-v1entries[] (newest first) — the same recorded writes restore --json lists, keyed for timeline display
agentstack workflow list --jsonworkflow-observe-v1workflows[] with per-entry trust and lock state
agentstack workflow list --jsonworkflow-serial-roles-v1per-entry serial_roles
agentstack workflow list --json / workflow explain --jsonworkflow-role-selection-v1per-entry role_details[] — each role's harness, model, effort, serial, and any declared value that would not reach the child. explain carries the envelope too; it is the deeper per-workflow read and re-gates on trust
agentstack workflow runs --jsonworkflow-observe-v1runs[] from the machine-global runs directory
agentstack more lease status --jsonlease-status-v1leases[] — the machine-level runtime lease registry, each row's liveness derived at read time from the PID and that process's start time. unknown never means live. Writes nothing; on macOS it does run /bin/ps per recorded PID to read a start time, because there is no /proc to read instead
agentstack more delivery --jsondelivery-routing-v1default plus one harnesses[] row per targeted CLI with its per-kind routes[] (where the bytes go) and that harness's own bridge_registered. Decide on those two typed fields; the row's summary and why are display copy and must never be matched on
agentstack more image --jsonimage-plan-v1the packaging plan: every pinned members[] entry, required_secrets (names only), blockers, buildable
agentstack status --jsonlibrary-sources-v1project.shadowed_names[] — one sentence per capability name more than one linked library source holds. Always present, [] when nothing collides
agentstack status --jsoninstruction-channels-v1project.instruction_channels[] — one row per targeted CLI, including the ones with no instruction channel at all
agentstack status --jsonpackage-members-v1project.packages[] — the effective member set this project pinned, after its overrides. Inserted only when a package is selected
agentstack status --jsonneeds-your-yes-v1project.needs_your_yes — present only when calls were actually refused here since the last yes. Carries a count and the fix, never a card
agentstack status --jsonupdate-offer-v1project.updates — an offer, never a currency claim: the check is offline, so a missing key is not "up to date"
agentstack init --planinit-planthe detection plan, with plan_digest
agentstack trust --previewtrust-previewthe full reviewed surface, with surface_digest
agentstack trust --previewtrust-server-blockers-v1known server/executable blockers, each with a fix of agentstack lock --write or edit-manifest
agentstack trust --previewtrust-review-card-v1the per-item review card a graphical client renders — the first-time surface and, on a re-gate, the changed-lines diff
agentstack trust --previewtrust-card-diff-v1review.items[] and review.removed[] — the card itself, structured, with a change marker per item
agentstack trust --previewtrust-card-groups-v1review.groups[], holding indices into review.items, plus review.question — the one closing question. There is no per-group or per-item question, accept, or block, and there never will be
agentstack library-indexprofiles-edit-v1the central-library catalog (skills + servers)

These are not reads. Each one previews first, returns a digest over exactly what it proposes, and then refuses to apply if the inputs moved underneath it. Pass the digest back to apply.

CommandContract nameNotes
agentstack init --yes --consented <digest>apply-setuprefuses when the detected inputs drifted since the plan
agentstack trust --yes --consented <digest>trust-consentgrants bound to the previewed bytes; refuses stale or missing digests
agentstack add-skill-to-profileprofiles-edit-v1re-locks and re-renders
agentstack add-server-to-profileprofiles-edit-v1re-locks and re-renders
agentstack use-profileprofiles-edit-v1re-locks and re-renders
agentstack create-profiletoolset-create-v2writes the manifest entry and re-locks, and renders nothing — naming a toolset is not activating it
agentstack edit-profileprofiles-edit-batch-v1one preview + one digest over a batch of membership edits
agentstack toolset renametoolset-rename-v1renames the toolset everywhere it appears; memberships kept
agentstack toolset deletetoolset-delete-v1deletes the toolset; the servers and skills in it stay declared
agentstack set-modeset-mode-v1superseded, refusesthe Mode axis retired; delivery is routed, agentstack status reports it, and agentstack more uninstall removes rendered files
the managed-.gitignore prompt on apply / use --writegitignore-opt-out-v1records a durable per-project opt-out from the managed block
agentstack remove-from-librarylibrary-remove-v1machine-wide, not project; recoverable from lib/.trash
agentstack remove-capabilitymanifest-remove-v1removes a project definition and memberships, then re-locks and re-renders; library untouched
agentstack more restore --last --writerestore-lastundoes the newest recorded write

profile in these command and contract names is the older spelling of toolset; they name the same object.

Payload shapes#

status --json#

The orientation screen, keyed. Branch on project: it is null whenever the manifest is absent or unreadable, and manifest.error says which.

json
{
  "version": "0.18.0",
  "clis_detected": ["Claude Code", "Codex CLI", "Gemini CLI"],
  "manifest": {
    "path": "/repo/.agentstack/agentstack.toml",
    "present": true,
    "loaded": true,
    "error": null
  },
  "project": {
    "servers": 2,
    "skills": 0,
    "targets": { "pinned": [], "fanout": 6 },
    "toolsets": ["dev", "writing"],
    "session": {
      "profile": "dev",
      "started_unix": 1785179964,
      "age_seconds": 27,
      "abandoned": false
    },
    "locked": true,
    "trust": "trusted",
    "trust_relevant": false,
    "mode": "static",
    "gateway_connected": false,
    "rendered": true,
    "secrets": { "referenced": 1, "unresolved": ["NOTION_TOKEN"] }
  },
  "next_action": {
    "command": "agentstack doctor",
    "why": "verify the wiring — every warning names its fix"
  }
}

Field notes:

search --json#

json
{
  "query": "notion",
  "results": [
    {
      "name": "smithery-notion",
      "id": "ai.smithery/smithery-notion",
      "description": "A Notion workspace is a collaborative environment…",
      "source": "registry",
      "kind": "server",
      "details": null,
      "in_manifest": false,
      "trust": {
        "namespaced": true,
        "runs_code": false,
        "needs_secret": true
      },
      "add_command": "agentstack add from ai.smithery/smithery-notion"
    }
  ]
}

Field notes:

adapters list --json#

json
{
  "adapters": [
    {
      "id": "claude-code",
      "display": "Claude Code",
      "installed": true,
      "config_present": false,
      "status": "installed",
      "origin": "built-in"
    }
  ]
}

Field notes:

session list --json#

json
{
  "sessions": [
    {
      "dir": "/repo/.agentstack",
      "profile": "dev",
      "scope": "project",
      "started_unix": 1785179964,
      "age_seconds": 14,
      "abandoned": false
    }
  ]
}

Field notes:

delivery --json#

json
{
  "default": "automatic",
  "harnesses": [
    {
      "id": "claude-code",
      "display": "Claude Code",
      "mcp_capable": true,
      "render_locally": false,
      "override": "none",
      "bridge_registered": false,
      "summary": "skills + MCP servers planned live (not connected) · house rules + settings + hooks written to files",
      "routes": [
        {
          "kind": "servers",
          "lane": "dynamic",
          "why": "the live channel here can carry it on demand",
          "full_ceremony": false
        }
      ]
    }
  ]
}

routes carries one row per capability kind — skills, servers, instructions, settings, hooks — trimmed to one here.

Field notes:

edit-profile --preview (profiles-edit-batch-v1)#

The one membership verb that can also take things out of a toolset, and the only one whose cost does not scale with the number of changes: every add and every removal lands as one manifest write under one consent_digest, followed by a single re-lock and re-render.

json
{
  "profile": "backend",
  "add_skills": ["rust-testing"],
  "add_servers": [],
  "remove_skills": [],
  "remove_servers": ["github"],
  "skills": ["rust-testing"],
  "servers": [],
  "empties_toolset": false,
  "action": "edit-profile",
  "consent_digest": "sha256:b428c87f…2111d8",
  "note": "Review, then apply with --yes --consented sha256:b428c87f…2111d8…",
  "schema_version": 1,
  "features": ["…"]
}

Field notes:

JSON that is not part of this contract#

Some commands emit JSON that is evidence or analysis rather than control-plane state. These carry no envelope and no feature name, and their shape may change without a schema_version bump. Treat them as reports, not APIs:

The append-only evidence files are the same kind of thing: JSON Lines with no envelope and no feature name, self-describing per row, and free to gain variants. Three moved recently and are worth knowing if you parse them:

Recorded is not prevented. What those files prove about enforcement is answered in the enforcement matrix, not here.

Guarantees#

Source of truth: docs/automation.md — this page is generated from it.