aasm integrations
Install, verify, repair and remove Developer Integrations for AI dev tools — the governance wiring that makes a tool like Claude Code run through Agent Assembly instead of straight out to its provider.
Absent from
cargo install aasm— and only from there.aasm integrationsis a developer-only command group. Likeaasm runandaasm tools, it is gated behind thedevtoolregion inaa-cli/src/commands/mod.rsandaa-cli/Cargo.toml, and.ci/strip-for-publish.shremoves that region in thepublish-cratesjob ofrelease.yml— the crates.io publish and nothing else. A source build (cargo build -p aa-cli), the GitHub Release tarballs, thecurlinstaller and the Homebrew formula are all built from the unstripped tree, so they do carry this command. Where the strip does apply it is not cosmetic: a crates.ioaa-runtimenever binds the DI-API socket this command talks to, so the surface would have nothing to connect to.
This page is the command reference — subcommands, flags, defaults, exit
codes. For what the lifecycle means (profiles, evidence, protection levels,
what is and is not measured), read
aasm integrations in the Developer Integrations section
and Protection levels.
What it is, in one paragraph
aasm integrations is a client of the
Developer Integration API and
nothing more. It holds no per-tool knowledge, performs no mutation of its own,
and never derives a protection state locally: every per-tool fact arrives over
one Unix socket from an adapter inside the trusted aa-runtime, and every
mutation happens there. That is why the command needs a running runtime —
there is no in-process fallback, by design
(ADR 0030
§7.1).
Invocation
aasm integrations [OPTIONS] <COMMAND> [ARGS]
| Subcommand | Argument | Mutates | Purpose |
|---|---|---|---|
list | — | no | Detected tools, compatibility, integration state, protection |
plan <TOOL> | tool id | no | Exactly what an install would change |
install <TOOL> | tool id | yes | Apply, after showing the changes and the permissions |
status <TOOL> | tool id | no | The protection level and the evidence behind it |
verify <TOOL> | tool id | no | Run the protection test and report what it established |
repair <TOOL> | tool id | yes | Restore AASM-owned state that drifted |
remove <TOOL> | tool id | yes | Undo the integration, restoring what it replaced |
<TOOL> is the tool id as aasm integrations list reports it —
claude-code, codex, github-copilot, windsurf-cascade.
These same ids are accepted by aasm run, which
has its own shorter canonical spellings (claude, codex, copilot,
windsurf). An id copied out of aasm integrations list launches the tool it
names; the two commands do not have separate vocabularies.
Only
claude-codehas a lifecycle today. The other three are carried byLegacyAdapterShim: they detect and report, butinstallandrepairare refused with exit3because their plan steps name no destination file. That refusal is deliberate — a success that performed nothing would be worse. Being listed means the tool is recognised, not that it can be integrated; see Limitations.
Options common to every subcommand
The global options (--context, --output,
--api-url, --api-key) apply, plus one flag defined on the group itself:
| Flag | Default | Description |
|---|---|---|
--no-autostart | off | Report a stopped runtime (exit 7) instead of starting one. |
--allow-unverified-runtime | off | Proceed against a runtime whose build cannot be shown to be this one. See 10 and 11. |
--no-autostart
Lifecycle commands need a running Agent Assembly runtime. By default aasm
starts one, says so on stderr, and waits:
$ aasm integrations list
Starting Agent Assembly runtime…
A missing socket is a bootstrap action, not a transient error — it is never
silently retried. Pass --no-autostart in CI, where leaving a daemon behind is
worse than failing; the missing runtime then becomes exit code 7
(runtime_unavailable) instead.
--output json
--output json (and --output yaml) emit the same model the human table is
rendered from, so anything readable is parseable. Reports go to stdout;
notices, prompts and errors go to stderr, so
aasm integrations status claude-code --output json | jq stays valid even when
the runtime had to be started first.
Machine-readable output also makes the mutating commands non-interactive: there
is nothing on the other end that can answer a prompt, so install, repair and
remove abort (exit 9) rather than block, unless --yes is passed.
Consent: --yes/-y
install, repair and remove share one consent model (AAASM-6085).
--yesskips Agent Assembly’s own confirmation prompt only. It never bypasses operating-system administrator authorization or another security boundary.
-y is a short alias for --yes on all three commands. Neither form:
- suppresses the material-change disclosure printed before the prompt —
--yesskips answering the question, not showing what it would apply; - implies
--force(remove’s separate flag for proceeding with a known- incomplete reversal); - widens what a plan touches, or turns a warning/error into a success;
- answers a host-level authorization prompt. A plan with a privileged step
(
--install-managed-settings, or any--allow-privileged-host-stepsentry) still reaches Touch ID, an admin password,sudo, or the platform equivalent honestly —--yeshas no path into that boundary. In a non-interactive session with no way to satisfy it, the run fails closed with no partial write, the same as it would without--yes.
Without --yes, non-interactively: the command aborts (exit 9) before
sending any mutating request, and the message names --yes as the fix. Zero
mutation happens either way.
With --yes: the command’s own prompt is skipped and the run proceeds
through every ordinary, unprivileged step automatically. A privileged step is
still attempted honestly, not bypassed — see above.
Distinguishing consent from host authorization in automation. --output json reports consent_auto_approved: true when --yes/-y skipped this
command’s own prompt (false on a preview, a no-op, or an interactive run
that answered the prompt itself). This tells automation “AASM’s own gate was
auto-approved” apart from “there was nothing to ask about”. It does not
attempt a second, finer distinction between “host authorization unavailable”
and “host authorization refused” — both surface as the same ordinary command
failure (the ordinary non-zero exit and disclosure this document already
describes for a denied/unavailable authorization), consistent with the
DI-API’s own deliberately coarse deny codes (§5.3): this client does not
infer more from a message’s wording than the wire states structurally. A
caller that needs that finer split should track it as a follow-up rather
than assume this document promises it.
LLM / coding-agent / CI usage. --yes --output json is the shape an
agent or pipeline should use: no prompt to hang on, and a parseable report
including consent_auto_approved and outcome. True unattended execution is
possible only when the plan has no privileged step (--profile recommended
without --install-managed-settings/--allow-privileged-host-steps) — a
plan that does still needs an operator present for the OS prompt, or a
pre-authorized OS mechanism (e.g. a pre-approved Touch ID/PAM policy) outside
Agent Assembly’s control. Before automating a repeated mutation (e.g. a
scheduled repair --yes), run it once without --yes to review the plan by
hand — an automated “yes” to a prompt you have not read is exactly the risk
this flag exists to bound, not remove.
Rollback and failure. A privileged step that fails leaves no partial
state — the engine reverses everything the run had already applied — whether
or not --yes was passed. aasm integrations remove is the general-purpose
rollback for anything that did install; see below.
Copyable examples:
$ aasm integrations install claude-code --install-managed-settings --profile strict --yes
$ aasm integrations install claude-code --install-managed-settings --profile strict -y --output json
$ aasm integrations repair claude-code --yes --output json
$ aasm integrations remove claude-code -y --output json
aasm integrations list
aasm integrations list [OPTIONS]
| Flag | Default | Description |
|---|---|---|
--capabilities | off | Show every declared mechanism per tool, not just the summary row. |
aasm integrations plan <TOOL>
Mutates nothing. Prints the material changes an install would make, the permissions it would need, and the mechanisms the tool cannot use with the reason.
| Flag | Values | Default | Description |
|---|---|---|---|
--profile | recommended | strict | observe-only | recommended | Which protection profile to plan for. |
--scope | user | project | managed | user | Which configuration surface to write. Explicit, never inferred from the working directory. |
--policy-profile <NAME> | string | "" (service default) | The policy profile to resolve, by name. The document itself never crosses this boundary. |
--allow-privileged-host-steps | flag | off | Include steps that change host state (trust stores, launch agents). |
--install-managed-settings | flag | off | Install the tool’s administrator-managed settings file. Implies --scope managed and the privileged-step consent. The file’s installation is verified by read-back; its enforcement is unmeasured. |
The --profile tokens are what you type; the wire tokens the DI-API receives
are recommended, strict and observe_only. observe-only computes and
audits every decision and applies none of them, and is never displayed as
protection — status says monitoring.
Why --scope managed alone is refused
--scope managed reads like a third choice next to user and project, and it
says nothing about administrator authorization. On its own it is therefore
rejected with exit 9 (aborted) and a remediation naming the flag that does
mean consent:
$ aasm integrations plan claude-code --scope managed
error: nothing was changed: writing the administrator-managed settings surface needs an explicit opt-in
--install-managed-settings is that opt-in. It selects the managed surface,
carries the privileged-step consent, and asks for administrator authorization
for one file write — the settings surface the tool documents as
non-overridable. It is the only route to Host Enforced, it is off by default,
and the default install stays fully unprivileged.
Before you are asked to approve anything, the plan states the exact path, the exact content and its SHA-256, the diff against what is on the host, any conflict, and the backup and rollback behaviour. An unavailable or denied authorization is a truthful failure, never a quieter install; a non-interactive run fails immediately rather than waiting for credentials.
Host Enforcedmeans the policy is installed where you cannot rewrite it. It does not mean a bypass was demonstrated to fail — see Limitations. The procedure that would close that gap is Measuring managed-settings enforcement.
aasm integrations install <TOOL>
Takes every plan flag above, plus:
| Flag | Default | Description |
|---|---|---|
--yes, -y | off | Apply without asking. Required for non-interactive and machine-readable runs. See Consent. |
--dry-run | off | Show the plan and stop, exactly as plan does. |
The preview you approve is the same plan object that gets applied — not a second
rendering of it — so you cannot consent to something you were not shown.
Silence is not consent: without a terminal and without --yes, the command
aborts and changes nothing.
aasm integrations status <TOOL>
No flags beyond the common ones. Reports the achieved protection level and the observation that justifies it, split by how the observation was obtained (exercised vs read-back vs could-not-be-checked), including the rungs this host cannot reach. The timestamp is part of the claim: a status says “verified at T”, not “true now”.
Gateway Protected is reported only on adjudicated exercised evidence.
Configuration that reads back correctly justifies at most Integrated.
aasm integrations verify <TOOL>
No flags beyond the common ones. Runs the adjudicated protection exercise and
exits 0 only when the protected path was actually exercised and the outcome
was protective. Otherwise it exits 6 — read that as “not measured”, never
as “measured and failed”. The probe uses a synthetic secret chosen by the
adapter and run by the service; no real credential is read, sent or printed.
aasm integrations repair <TOOL>
| Flag | Default | Description |
|---|---|---|
--dry-run | off | Show what drifted and stop. |
--yes, -y | off | Repair without asking. Required for non-interactive and machine-readable runs. See Consent. |
Repairing nothing is a success and exits 0 — see
Outcome: did the world change? below, which is
what tells a restored integration from one that never needed restoring.
nothing_to_repair additionally says which no-op it was: no receipt accounts
for the tool at all, or the AASM-owned state already matches the receipt it has.
aasm integrations remove <TOOL>
| Flag | Default | Description |
|---|---|---|
--dry-run | off | Show the restoration actions and stop. |
--yes, -y | off | Remove without asking. Required for non-interactive and machine-readable runs. See Consent. |
--force | off | Proceed even when the reversal is known to be incomplete. |
Removal is derived from the receipt, not re-derived from current host state:
it undoes what was done, not what would be done now. Anything that cannot be
undone automatically is printed as a residual action first, every time;
--force only answers “yes, remove anyway and leave those behind” and never
removes anything the plan did not name.
Restoration is semantics-exact, not byte-exact — the keys Agent Assembly owns are removed and the prior values restored, but formatting and key order in a file someone else also writes are not guaranteed to be reproduced verbatim.
Removing an integration that is already gone is a success and exits 0, so
a teardown loop does not have to special-case its second run. What tells the two
runs apart is the outcome below.
Outcome: did the world change?
The exit code answers did the command succeed?. It does not answer did
the world change?, and the two are different questions: a remove of an
integration that is already absent succeeded and modified nothing. Overloading
one code with both is how aasm integrations repair X && echo repaired came to
print “repaired” for a tool that was never installed.
So no exit code was minted for a no-op. A legitimate no-op is a successful
idempotent outcome and exits 0, and the mutation question is answered by a
separate, explicitly reported outcome:
| Outcome | Meaning | Exit |
|---|---|---|
changed | The requested end state was reached, and something was modified. | 0 |
unchanged | The requested end state already held; nothing was modified. | 0 |
refused | The command declined to act — authorization, policy, consent, invalid input. Nothing was modified. | non-zero |
failed | The command tried and did not reach the requested end state. | non-zero |
changed and unchanged are reported on the result’s first line and as
outcome in --output json / --output yaml. refused and failed are named
on stderr, beside the specific exit code from the table below — stdout stays
empty on a non-zero exit, so a QA harness has no result to record from a run
that refused.
Branching on it
This is wrong, and is the defect this contract exists to prevent:
# WRONG — prints "repaired" for a tool that was never installed.
aasm integrations repair claude-code --yes && echo repaired
This is right:
case $(aasm integrations repair claude-code --yes --output json | jq -r .outcome) in
changed) echo 'drifted state was restored' ;;
unchanged) echo 'nothing needed repairing' ;;
esac
Which commands report it
| Command | Reports outcome | Notes |
|---|---|---|
repair | yes | changed when the service restored something; unchanged for both no-op states. |
remove | yes | changed when the reversal ran; unchanged when there was no integration to remove. |
install | yes, from DI-API 5 | changed when the apply modified the host; unchanged when the exact desired managed state already existed. null when the runtime is too old to say — see below. |
list, plan, status, verify | no | None of them is asked to reach an end state on the host, so neither token would mean anything. verify has its own pass/fail axis in outcome on its own report — that field is the verification result (passed, partially_passed, failed, unverifiable), not this vocabulary. |
A --dry-run reports null rather than a token. It changed nothing, but it
also did not establish that the end state already holds — the drift it is
previewing is proof of the opposite. The one exception is a --dry-run against
a tool with no integration at all: that state is settled before any plan is
previewed, so it reports unchanged.
install, and what null means there
install reports the outcome only when the runtime states it, which needs
DI-API 5 or newer (AAASM-5674). The engine has always computed the fact; before
v5 the wire had nowhere to carry it.
Against an older runtime the report carries "outcome": null and an
outcome_unknown string saying why:
$ aasm integrations install claude-code --yes
…
Applied as receipt receipt-claude-code-user-1786078344
outcome: not reported — this runtime speaks DI-API v4; the apply
outcome arrived in v5, so it could not be asked whether
anything changed
{
"outcome": null,
"outcome_unknown": "this runtime speaks DI-API v4; the apply outcome arrived in v5, …"
}
null is not unchanged. It means no answer was established, and a script
must handle it as its own case:
case $(aasm integrations install claude-code --yes --output json | jq -r .outcome) in
changed) echo 'the tool was configured' ;;
unchanged) echo 'it was already exactly as planned' ;;
null) echo 'this runtime cannot say; update it' ;;
esac
The exit code is still 0 in the null case: the apply itself succeeded. What
could not be established is a claim about the host, not about the command —
which is precisely why the exit code cannot answer this question.
aasm does not guess the missing answer, and neither should another client.
Each available substitute is wrong in a way that produces a confident false
unchanged:
| Substitute | Why it is wrong |
|---|---|
receipt_id | Reused when the plan id matches, whether or not anything mutated — a no-op reapply deliberately keeps the prior receipt so the store’s history does not record an upgrade that never happened. |
applied_at_unix_secs | A cross-process, second-granularity clock compare. Two installs inside one second are indistinguishable, and it false-reports changed in a loop. |
a status read before the apply | Carries neither receipt_id nor plan_id, so it cannot match “the exact desired managed state”. Swapping the policy profile at the same planned_level would read as a false unchanged. |
| the exit code | Answers the other axis entirely. |
If you need the answer from an older runtime, update the runtime. Comparing
aasm integrations status before and after is a weaker fallback: it cannot see
a change that leaves the reported level identical.
Exit codes
aasm integrations gives every outcome its own code so a wrapper can branch on
the code rather than parse English out of stderr. The table below is generated
from aa-cli/src/commands/integrations/exit.rs and printed by
aasm integrations --help.
These answer did the command succeed? only. For did the world change? see
Outcome above — a no-op exits 0 here and is
distinguished there, never by a code of its own.
| Code | Name | Meaning |
|---|---|---|
0 | success | The operation completed. |
1 | internal_error | A transport or lifecycle failure. |
3 | unsupported | The tool, mechanism or verb is not available here. |
4 | incompatible | This client, the core or the tool version do not agree. |
5 | drifted | AASM-owned state no longer matches its receipt — run repair. |
6 | verification_failed | The protection test did not establish protection. |
7 | runtime_unavailable | No runtime is listening and none could be started. |
8 | denied | The runtime refused this client — re-enrol or fix permissions. |
9 | aborted | Nothing was changed — declined, or no confirmation was possible. |
10 | runtime_unverified | The runtime that answered was shown not to be this build — stop it and re-run. |
11 | runtime_unverifiable | The runtime that answered carries no build identity, so nothing was established either way. |
2 is deliberately unused. clap exits 2 for a usage error, so reusing it
would make “you typed the command wrong” indistinguishable from a real outcome.
aasm integrations verify claude-code || case $? in
6) echo 'protection not measured — treat as unprotected, do not report a failed block' ;;
5) aasm integrations repair claude-code --yes ;;
esac
10 and 11 — which build answered
These two are about the runtime that served the command, not about the tool it
was asked about. A reachable socket is not evidence that the right thing
answered: a runtime built from another checkout, or one whose executable has been
deleted, answers perfectly well and describes its host. That is how a healthy
Claude Code once got reported as not_installed.
Every aasm integrations command therefore checks which build answered, before
producing any output.
| Code | Standing | When |
|---|---|---|
10 runtime_unverified | refuted | The runtime was shown not to be usable as this build: a different build_sha or core_version, an executable_path that no longer exists, or more than one runtime listening at once. A positive finding. |
11 runtime_unverifiable | unverifiable | The runtime’s identity could be neither confirmed nor refuted: one or both sides carry no authoritative build identity, or the peer predates DI-API v4 and cannot state one. An absence, not a finding. |
Which commands emit which:
| Command | Reads or writes | Exit 10 | Exit 11 |
|---|---|---|---|
aasm integrations list | read-only | yes | no — answers, and reports unverifiable |
aasm integrations plan | read-only | yes | no — answers, and reports unverifiable |
aasm integrations status | read-only | yes | no — answers, and reports unverifiable |
aasm integrations install | writes host state | yes | yes |
aasm integrations verify | asserts enforcement is established | yes | yes |
aasm integrations repair | writes host state | yes | yes |
aasm integrations remove | writes host state | yes | yes |
Read-only commands still answer under an unverifiable standing because refusing
them would make the situation undiagnosable — they are exactly the commands you
use to find out which runtime answered and stop the wrong one. They say so on
stderr, and --output json carries the standing so a recorded result stays
marked:
"runtime": {
"provenance": {
"standing": "unverifiable", // verified | unverifiable | refuted
"verdict": "unverifiable", // the specific fact behind the standing
"build_sha": "unknown",
"build_id_source": "absent", // injected | checkout | packaged | absent
"pid": 24601,
"fields": [ // which facts were absent, matched, mismatched
{ "field": "build_sha", "status": "absent", "expected": "unknown", "reported": "unknown" }
],
"reachable_runtimes": 1
}
}
unverifiable is never reported as verified, on any surface or in JSON.
Branch on standing, not on the presence of a build_sha.
reachable_runtimes is one-directional evidence. Above one it proves
ambiguity — each of those sockets was connected to, so each of those runtimes
exists, and the result cannot be attributed to one of them. Equal to one it
proves only that nothing else was found: the scan probes files named
devint*.sock, in the answering socket’s own directory, once as the session
opens. A runtime under another name, in another directory (which
AA_DEVINT_SOCKET makes trivial), or started a moment later is not counted.
Read 1 as “no duplicate was observed”, never as “this is the only runtime”.
standing is the one field that folds in every reason a result may not be
attributable, which is why it is the only one a wrapper needs to read.
verdict is narrower — it reports the identity comparison alone, and two
runtimes compiled from one commit have identical identities, so verdict reads
verified for both of them. standing cannot read verified while
reachable_runtimes is above one.
A wrapper that records evidence should refuse anything but verified:
aasm integrations status claude-code --output json > result.json || case $? in
10) echo 'the wrong runtime answered — stop it and re-run'; exit 1 ;;
11) echo 'the runtime carries no build identity'; exit 1 ;;
esac
jq -e '.runtime.provenance.standing == "verified"' result.json \
|| { echo 'result is not attributable to this build'; exit 1; }
--allow-unverified-runtime downgrades both refusals to a stderr warning for a
deliberately mixed installation. It does not change what is reported: the
standing reaches --output json and rides above the result in the table
rendering, so a result obtained through it stays marked as unverified rather
than passing as verified.
It also disarms the multiplicity refusal, which is not an identity cause at
all. With more than one runtime reachable the command answers from whichever one
it connected to and the others are never consulted; reachable_runtimes says how
many there were, and standing cannot read verified while that is above one.
Environment
| Variable | Read by | Effect |
|---|---|---|
AA_DEVINT_ENABLED | aa-runtime | Must be truthy for the runtime to serve the DI-API at all. Off by default. |
AA_DEVINT_SOCKET | runtime + clients | Overrides the DI-API socket path (~/.aa/run/devint.sock). |
AA_DEVINT_TOKEN_FILE | runtime + clients | Overrides the capability-token (enrolment) file path. |
AASM_STATE_DIR | aa-core, aa-proxy | Root of the integration receipt store (${AASM_STATE_DIR:-~/.aasm}/integrations/). |
See Configuration → Environment variables for the full table and the file modes these paths are held to.
See also
aasm integrations— the lifecycle explained- Onboarding a Developer Integration — the same journey as a walkthrough, with troubleshooting.
- Protection levels
- Limitations and known bypasses
- Developer Integration API
Last updated: 2026-09-11 by Chisanan232