ACP guard
Put DefenseClaw between an ACP editor client and coding agent to inspect prompts, streaming output, permissions, filesystem, terminal, and extension methods.
The ACP guard is a local, protocol-aware security boundary between an editor and an agent. It lets Zed, JetBrains, or another ACP client keep its native agent UX while every JSON-RPC frame passes through DefenseClaw policy.

Use cases
- Run Kiro in Zed or JetBrains without giving the editor an unmediated Kiro process.
- Apply one guardrail profile to several native ACP agents instead of building a different extension for every IDE/agent pair.
- Observe prompt, output, permission, filesystem, terminal, tool, and vendor-extension traffic with canonical observability v8 records.
- Block a denied method or unsafe content before it crosses the editor-agent boundary.
- Let an organization allow only selected client/agent pairs, such as
zed + kiro, in a policy profile.
ACP complements connector hooks. Hooks observe vendor lifecycle events inside an agent. ACP protects the editor-to-agent protocol boundary, including client filesystem and terminal callbacks that a hook contract may not expose.
Quick start: Kiro in Zed
Build or install DefenseClaw, start the gateway, and configure the managed entry:
defenseclaw-gateway start
defenseclaw acp setup --client zed --agent kiro
defenseclaw acp statusSetup defaults to observe. Review the events, then explicitly activate blocking:
defenseclaw acp setup --client zed --agent kiro --activateFor JetBrains, replace --client zed with --client jetbrains. The command
preserves unrelated JSON keys and foreign agent entries. It writes one
DefenseClaw · Kiro entry, a mode-0600 scoped token, and a runtime contract
lock. A failed config write rolls all three changes back.
Finding what is already unguarded
An editor may already be launching an ACP agent directly. That is exactly the traffic the guard exists to mediate, so DefenseClaw reads the clients it knows and says so rather than waiting to be asked about a specific pair:
defenseclaw acp detect zed — /Users/you/.config/zed/settings.json
guarded DefenseClaw · Kiro
⚠ UNGUARDED cursor → launched from the client's own registry; replace with a guarded cursor entry
foreign Something Else (left untouched)
installed but not configured here: devin, hermes, opencodeEach agent server falls into one class:
| Class | Meaning |
|---|---|
guarded | The entry launches defenseclaw-acp |
adoptable | A direct launch of a native catalog agent — the guard can wrap it |
client_registry | The client resolves and launches the agent itself, so there is no argv to wrap. Unmediated, and adoption cannot close it |
foreign | Not an ACP agent DefenseClaw knows. Never rewritten |
Then take the adoptable ones over:
defenseclaw setup acp # or: defenseclaw acp adoptAdoption runs the same acp setup transaction per pair — same executable
resolution, digest pinning and contract lock — and then points the entry you
already had at the guard, keeping its key and its env. Your familiar picker
entry stops reaching the agent unmediated; it does not sit beside a guarded
duplicate, because that would leave a bypass the guard cannot see. Re-running
finds nothing to adopt rather than wrapping twice. Adoption defaults to
observe; pass --activate for action mode.
First-run names the editors with unguarded agents and points here, so this is visible without knowing the command exists.
A client_registry entry cannot be adopted. DefenseClaw installs a guarded
entry for the same agent when it has a native entry point, but the registry
entry remains a way to launch that agent without the guard until you remove it
in the client. detect keeps reporting it for exactly that reason.
Several guarded agents in one editor
Nothing stops an operator from guarding more than one agent in the same editor. Run setup once per agent:
defenseclaw acp setup --client zed --agent kiro
defenseclaw acp setup --client zed --agent devinEach run writes its own entry (DefenseClaw · Kiro, DefenseClaw · Devin),
leaves the other entries and every foreign agent server untouched, and pins its
own runtime contract lock at <data-dir>/acp/<client>-<agent>.contract-lock.json.
The editor launches one defenseclaw-acp process per entry, so the two
sessions are independent: separate stdio pipes, separate turn buffers, separate
pending-ID accounting. A block in one does not interrupt the other.
Policy is per pair. Each <client>/<agent> binding resolves its own
profile, so one editor can run one agent in action mode while another stays in
observe:
acp:
enabled: true
default_profile: locked
clients:
zed: {enabled: true} # the pin records only that the half is enabled
agents:
kiro: {enabled: true}
devin: {enabled: true}
bindings:
zed/kiro: {enabled: true, profile: locked} # action
zed/devin: {enabled: true, profile: watch} # observe while it earns trust
profiles:
locked:
mode: action
fail_mode: closed
allowed_clients: [zed]
allowed_agents: [kiro]
denied_methods: [fs/write_text_file]
watch:
mode: observe
allowed_clients: [zed]
allowed_agents: [devin]defenseclaw acp status reports each binding's resolved profile and whether it
came from the binding or was inherited:
"zed/kiro": {"profile": "locked", "mode": "action", "profile_source": "binding"}
"zed/devin": {"profile": "watch", "mode": "observe", "profile_source": "binding"}Resolution is most-specific-first: the bindings entry, then the agent pin,
then the client pin, then default_profile. A configuration with no
bindings therefore behaves exactly as it did before they existed.
Two boundaries a per-pair binding does not cross:
- Disabling a client or an agent still disables every pair that uses it. A binding adds policy; it is not a way around either switch.
- The resolved profile must still admit the pair through
allowed_clientsandallowed_agents.
A request naming a profile the configuration does not assign to that pair is refused rather than evaluated under either name. The guard's profile is pinned into its contract lock at setup, so a mismatch means a stale or forged request, and the refusal says so rather than reporting the profile as undefined.
That makes editing a pair's profile by hand a two-step change: the guard keeps
the profile pinned in its argv until setup rewrites it. defenseclaw acp verify and defenseclaw acp status report the drift instead of calling the
binding healthy, so you find out before the next editor session fails closed:
ACP binding verification failed: contract lock profile 'locked' no longer
matches the configured 'watch' for zed/kiro; re-run acp setupOther consequences worth planning around:
allowed_agentsis the "only these agents" control. Removing an agent from the shared profile refuses its evaluations while leaving its editor entry in place; removing the entry withdefenseclaw acp remove --client zed --agent devinalso reclaims its contract lock.- In normal user mode the agents share one scoped token, the private file named by argv. Managed enterprise enrollment instead mints one credential per principal + client + agent + profile, so two agents in one editor hold two distinct bearers and revoking one leaves the other working.
- Each binding's digests are pinned separately. Upgrading the guard rebinds every matching user-owned lock in one transaction; a stale digest on one binding fails that binding closed without disturbing the other.
Managed enterprise enrollment
Managed deployments keep ACP policy and credential authority in the protected machine data directory. Each enrollment receives a unique credential scoped to one OS principal, client, agent, and profile. Before sending ACP content, the guard verifies a fresh, nonce-bound gateway challenge. It then encrypts the evaluation with AES-256-GCM under a challenge-derived key and HMAC-authenticates the ciphertext; the gateway HMAC-authenticates the exact verdict status and body. The credential and plaintext ACP content never cross an unauthenticated loopback socket. The gateway rejects a request whose declared binding differs from the machine-owned enrollment record.
An administrator or guardian first provisions the service record and the target user's private copy:
sudo /opt/defenseclaw/bin/defenseclaw-gateway enterprise acp enroll \
--user alice --client zed --agent kiro --profile kiro-only --jsonThe JSON result contains a token_file and a next command, never the token
value. Run the reported defenseclaw acp setup --managed ... command in the
target user's session, or deploy the equivalent Zed/JetBrains JSON through MDM.
Managed setup verifies the central allow-list, writes the editor launch entry
and executable contract lock in user-owned state, and cannot modify central
policy. The TUI exposes the same path under ACP Guard → Enroll managed
enterprise ACP.
Use the same exact selectors to verify or revoke an enrollment:
sudo /opt/defenseclaw/bin/defenseclaw-gateway enterprise acp verify \
--user alice --client zed --agent kiro --profile kiro-only
sudo /opt/defenseclaw/bin/defenseclaw-gateway enterprise acp revoke \
--user alice --client zed --agent kiro --profile kiro-onlyRevocation deletes the service-side authority first. A copied or cached bearer therefore stops authenticating even if user-side cleanup is interrupted. Linux and macOS guardians drop to the exact target uid/gid for the private copy; Windows uses the exact active-session SID token when the LocalSystem guardian targets another user. A user running the command for their own matching SID is also supported for controlled validation. Re-enrollment after revocation mints a different bearer, even when the protected service-owned credential directory already exists. Enrollment, rotation, and revocation hold a persistent OS file lock across the complete record/index transaction, so independent gateway, guardian, and administrator processes cannot interleave credential state.
The managed Windows payload and macOS managed runtime include
defenseclaw-acp as a separately hashed and protected executable. On Linux,
install it beside defenseclaw-gateway under /opt/defenseclaw/bin.
| UX and custody | Normal user mode | Managed enterprise mode |
|---|---|---|
| Policy owner | User config | Administrator-owned config; runtime mutation denied |
| Setup | One defenseclaw acp setup command or the ACP TUI panel | Admin enterprise acp enroll, then the reported user-side acp setup --managed command or MDM equivalent |
| Credential | One private local ACP token | Unique principal + client + agent + profile credential, with a private per-user copy |
| Enforcement rollout | Observe by default; --activate selects action | Central profile chooses observe/action; the user cannot activate or downgrade it |
| Revocation | Remove the guarded editor entry | Remove service authority first, then the user copy; stale bearers fail immediately |
| Readiness | Private local token and configured binding | Complete bounded record/index inventory and configured binding |
What is guarded
| Surface | ACP methods or messages | Protection |
|---|---|---|
| Protocol | initialize, auth, session lifecycle, cancellation, extensions | Strict JSON-RPC 2.0 NDJSON validation; no batches; 1 MiB frame and 128 pending-ID bounds |
| Prompt | session/prompt | Rules, scanners, optional judge, observe/action policy |
| Output | session/update and prompt response | Evaluated per frame and again as canonical per-field streams across the complete buffered turn; action mode releases nothing before the terminal prompt response passes |
| Approval | session/request_permission, elicitation/create | Allow or block; an unresolved confirmation verdict fails closed in action mode |
| Filesystem | fs/read_text_file, fs/write_text_file | Bidirectional content/method policy |
| Terminal | terminal/* | Bidirectional content/method policy |
| Extensions | for example cursor/* | Preserved as ACP extensions and evaluated as tool traffic |
The guard launches the agent with literal argv—never a shell. It connects only to a literal loopback address, disables environment proxying and redirects, and authenticates the gateway with a fresh HMAC challenge before encrypting each request with AES-256-GCM. The ACP-scoped credential has no authority on other API routes. A process that wins the configured loopback port sees neither the credential nor ACP plaintext and cannot forge an authenticated verdict, so the guard fails closed instead of trusting an impersonated gateway. The contract lock pins the selected guard and agent executable paths and SHA-256 digests, the exact client configuration path and digest, plus ACP schema-v1.21.0. Normal upgrades replace the guard and rebind every matching user-owned lock in one rollback-capable transaction, after first proving the old lock still matches the active guard. A managed lock is not rewritten by that transaction; it instead permits an administrator upgrade to change guard bytes only while the complete guard path continues to pass the platform root/Admin ownership, no-symlink, and no-untrusted-writer custody checks. Agent and editor configuration digests stay exactly pinned in both modes. If central policy mode and the launched contract mode drift, the guard stops the session even when the old contract was observe-only; rerun setup to make an observe-to-action promotion explicit.
Observability
Every guarded decision is emitted as a canonical
guardrail.evaluation.completed v8 event carrying these ACP dimensions:
| Attribute | Example |
|---|---|
defenseclaw.acp.client | zed |
defenseclaw.acp.agent | kiro |
defenseclaw.acp.method | session/prompt |
defenseclaw.acp.direction | client_to_agent, agent_to_client |
defenseclaw.acp.surface | prompt, output, permission, filesystem, terminal, tool, protocol |
defenseclaw.acp.profile | kiro-only |
defenseclaw.acp.protocol.version | schema-v1.21.0 |
The event keeps defenseclaw.guardrail.raw_action,
defenseclaw.guardrail.effective_action, and
defenseclaw.guardrail.would_block distinct, so an observe-mode
pass-through cannot be mistaken for an action-mode allow. Matched rules are
published in defenseclaw.guardrail.rule_ids with
defenseclaw.guardrail.finding_count, alongside
defenseclaw.guardrail.reason and defenseclaw.security.severity.
A blocked prompt therefore looks like this:
{
"defenseclaw.acp.client": "zed",
"defenseclaw.acp.agent": "kiro",
"defenseclaw.acp.method": "session/prompt",
"defenseclaw.acp.surface": "prompt",
"defenseclaw.guardrail.decision": "block",
"defenseclaw.guardrail.raw_action": "block",
"defenseclaw.guardrail.effective_action": "block",
"defenseclaw.guardrail.would_block": false,
"defenseclaw.guardrail.rule_ids": ["TRUST-IGNORE-PREVIOUS"],
"defenseclaw.guardrail.finding_count": 1,
"defenseclaw.security.severity": "CRITICAL"
}A profile-denied method blocks with finding_count 0 and no rule_ids: that
veto comes from the allow-list, not a scanner match, and the event does not
invent a finding to justify it.
Configuration
config_version: 8
acp:
enabled: true
mode: action
default_profile: kiro-only
clients:
zed: {enabled: true, profile: kiro-only}
agents:
kiro: {enabled: true, profile: kiro-only}
profiles:
kiro-only:
mode: action
fail_mode: closed
allowed_clients: [zed]
allowed_agents: [kiro]
denied_methods: [fs/write_text_file]This profile rejects a guard invocation claiming another client or agent. It
does not control an unrelated editor entry that launches an agent directly,
outside defenseclaw-acp. To make “Kiro only” an organization-wide guarantee,
combine the managed ACP binding with IDE policy/MDM and operating-system
application control that prevents unapproved agent binaries or configuration.
DefenseClaw deliberately reports that bypass boundary instead of claiming it
can govern a process it never receives.
In managed_enterprise, allowed_clients and allowed_agents must explicitly
name the enrolled pair; an empty wildcard allow-list is deliberately rejected
by enrollment even though it remains valid for ordinary local profiles.
Inventory and certification
ACP has a separate registry because client/agent protocol compatibility is not the same lifecycle as connector hooks or environment variables. The packaged snapshot records each entry point as native or bridge, its primary source, its upstream ACP Registry ID when one exists, and its DefenseClaw evidence level:
internal/inventory/acp_registry.jsoninventories the pinned protocol, clients, native/bridge agents, every DefenseClaw connector's ACP coverage, and the upstream registry version/date used for review.internal/inventory/acp_certifications.jsondefines evidence required before an integration can be called certified.cli/defenseclaw/inventory/hook_contracts.jsonrecords Kiro native hooks, which are installed as an independent second enforcement path. Their veto surface is resolved per request from the hook config that invoked them, not from the release. See Kiro; ACP needs none of that and vetoes every surface in the table above on its own.- ACP adds no secret environment variable. Agent-owned credentials remain in their existing stores; the guard token is a private file referenced by argv.
| DefenseClaw connector | ACP form | Entry point |
|---|---|---|
| Kiro | Native | kiro-cli acp |
| Cursor | Native | agent acp |
| OpenCode | Native | opencode acp |
| Hermes | Native | hermes acp |
| GitHub Copilot CLI | Native, public preview | copilot --acp --stdio |
| Devin | Native | devin acp |
The guard supports native ACP entry points only — an agent the vendor ships with an ACP mode of its own. Community bridges, vendor companion servers, and gateway-backed shims are deliberately out of scope: each adds a third-party process between DefenseClaw and the agent whose argv, schema and release cadence DefenseClaw does not control, which is the wrong place for a security boundary to depend on. Every other DefenseClaw connector therefore reports no ACP claim and is guarded through its native hook surface instead.
Listing an entry point means it was checked against primary documentation or
the upstream ACP Registry. It does not mean DefenseClaw has executed that
client/agent pair. Per-integration evidence states live in
acp_certifications.json, and the CLI never silently upgrades one based on a
live upstream registry response.
The CLI uses the packaged registry as its only executable inventory. It does not download or execute commands from the network registry during setup. Every selected guard and agent executable is resolved to an absolute path, hashed, and bound into the client-specific contract lock before the editor can launch it.
API and diagnostics
| Endpoint | Purpose |
|---|---|
POST /api/v1/acp/challenge | Empty, scoped request that authenticates the loopback gateway before ACP content is encrypted and sent |
POST /api/v1/acp/evaluate | Scoped, loopback-only, mutually authenticated frame evaluation |
GET /v1/acp/catalog | Compiled client/agent catalog |
GET /v1/acp/profiles | Effective profile inventory |
GET /health | ACP enabled/mode/schema/configuration/token readiness |
For managed mode, scoped_token_ready means the complete bounded enterprise
credential inventory is private and structurally valid. One malformed,
unexpected, or over-limit record fails the inventory closed.
The Kiro/Zed test binding has been exercised in both normal user mode and
managed_enterprise. Managed action and observe runs were completed on Linux
and native Windows, including live credential revocation, rotation, route
isolation, OS ownership/permission checks, and v8 evidence. macOS uses the same
protocol implementation and has release/package/trust tests; a real managed
run still requires a root-owned LaunchDaemon installation rather than a
user-owned development config.
Use defenseclaw acp catalog for the local catalog and
defenseclaw acp remove --client zed --agent kiro for surgical removal.
ZeptoClaw
ZeptoClaw routes through DefenseClaw via api_base redirect plus response-scan. Full guardrail pipeline runs on every request.
Capability Matrix
Per-connector breakdown of block capability, native ask events, fail-closed support, subprocess policy, and HITL behaviour. The single source of truth for "can this connector do X?"