Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 integrations is a developer-only command group. Like aasm run and aasm tools, it is gated behind the devtool region in aa-cli/src/commands/mod.rs and aa-cli/Cargo.toml, and .ci/strip-for-publish.sh removes that region in the publish-crates job of release.yml — the crates.io publish and nothing else. A source build (cargo build -p aa-cli), the GitHub Release tarballs, the curl installer 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.io aa-runtime never 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]
SubcommandArgumentMutatesPurpose
listnoDetected tools, compatibility, integration state, protection
plan <TOOL>tool idnoExactly what an install would change
install <TOOL>tool idyesApply, after showing the changes and the permissions
status <TOOL>tool idnoThe protection level and the evidence behind it
verify <TOOL>tool idnoRun the protection test and report what it established
repair <TOOL>tool idyesRestore AASM-owned state that drifted
remove <TOOL>tool idyesUndo 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-code has a lifecycle today. The other three are carried by LegacyAdapterShim: they detect and report, but install and repair are refused with exit 3 because 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:

FlagDefaultDescription
--no-autostartoffReport a stopped runtime (exit 7) instead of starting one.
--allow-unverified-runtimeoffProceed 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.

aasm integrations list

aasm integrations list [OPTIONS]
FlagDefaultDescription
--capabilitiesoffShow 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.

FlagValuesDefaultDescription
--profilerecommended | strict | observe-onlyrecommendedWhich protection profile to plan for.
--scopeuser | project | manageduserWhich 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-stepsflagoffInclude steps that change host state (trust stores, launch agents).
--install-managed-settingsflagoffInstall 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 Enforced means 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:

FlagDefaultDescription
--yesoffApply without asking. Required for non-interactive and machine-readable runs.
--dry-runoffShow 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>

FlagDefaultDescription
--dry-runoffShow what drifted and stop.
--yesoffRepair without asking. Required for non-interactive and machine-readable runs.

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>

FlagDefaultDescription
--dry-runoffShow the restoration actions and stop.
--yesoffRemove without asking. Required for non-interactive and machine-readable runs.
--forceoffProceed 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:

OutcomeMeaningExit
changedThe requested end state was reached, and something was modified.0
unchangedThe requested end state already held; nothing was modified.0
refusedThe command declined to act — authorization, policy, consent, invalid input. Nothing was modified.non-zero
failedThe 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

CommandReports outcomeNotes
repairyeschanged when the service restored something; unchanged for both no-op states.
removeyeschanged when the reversal ran; unchanged when there was no integration to remove.
installnot yetSee below.
list, plan, status, verifynoNone 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 does not report it yet. The runtime knows whether an apply mutated anything — the engine computes it — but the DI-API’s ApplyView does not carry the fact, and this client will not infer it from a receipt timestamp: a wrong unchanged tells a script the world did not change when it did, which is worse than no answer. Until the wire carries it, compare aasm integrations status before and after. Tracked on AAASM-5499.

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.

CodeNameMeaning
0successThe operation completed.
1internal_errorA transport or lifecycle failure.
3unsupportedThe tool, mechanism or verb is not available here.
4incompatibleThis client, the core or the tool version do not agree.
5driftedAASM-owned state no longer matches its receipt — run repair.
6verification_failedThe protection test did not establish protection.
7runtime_unavailableNo runtime is listening and none could be started.
8deniedThe runtime refused this client — re-enrol or fix permissions.
9abortedNothing was changed — declined, or no confirmation was possible.
10runtime_unverifiedThe runtime that answered was shown not to be this build — stop it and re-run.
11runtime_unverifiableThe 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.

CodeStandingWhen
10 runtime_unverifiedrefutedThe 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_unverifiableunverifiableThe 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:

CommandReads or writesExit 10Exit 11
aasm integrations listread-onlyyesno — answers, and reports unverifiable
aasm integrations planread-onlyyesno — answers, and reports unverifiable
aasm integrations statusread-onlyyesno — answers, and reports unverifiable
aasm integrations installwrites host stateyesyes
aasm integrations verifyasserts enforcement is establishedyesyes
aasm integrations repairwrites host stateyesyes
aasm integrations removewrites host stateyesyes

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

VariableRead byEffect
AA_DEVINT_ENABLEDaa-runtimeMust be truthy for the runtime to serve the DI-API at all. Off by default.
AA_DEVINT_SOCKETruntime + clientsOverrides the DI-API socket path (~/.aa/run/devint.sock).
AA_DEVINT_TOKEN_FILEruntime + clientsOverrides the capability-token (enrolment) file path.
AASM_STATE_DIRaa-core, aa-proxyRoot 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


Last updated: 2026-08-07 by Chisanan232