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.
List view for small screens. Use the expand button to open the drawing.
- EditorZed or JetBrains
- starts, stdioACP guard
- if set up separatelyDirect agent entry
- Direct agent entrynot guarded
- ACP guarddefenseclaw-acp
- checked messagesACP agent
- asks, encryptedGateway on loopback
- starts, stdioEditor
- Gateway on loopbackchecks each message
- evaluatesPolicy
- recordsAudit and telemetry
- ACP agentKiro, Cursor, …
- checked messagesACP guard
- Audit and telemetrylocal SQLite, OTLP
- Policyrules, scanners, judge
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.
- Record prompt, output, permission, filesystem, terminal, tool, and vendor-extension traffic in the audit log and your telemetry.
- 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
Install DefenseClaw, start the gateway, and add a guarded Kiro entry to Zed:
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
You can guard 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. Give each pair its own profile with --profile at setup (without it,
both pairs share the default profile, and setup refuses to change that
profile's mode for one pair only):
defenseclaw acp setup --client zed --agent kiro --profile locked --activate
defenseclaw acp setup --client zed --agent devin --profile watchSetup writes the binding, the profile, and the client and agent switches.
The resulting configuration looks like this, and you can tighten a profile
further by hand (here, denied_methods):
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 prints whether the guard is on, how many bindings it
has, and each binding's health, resolved profile and mode. defenseclaw doctor
lists the same bindings and fails on a broken one. defenseclaw acp status --json 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
In a managed deployment, the administrator owns ACP policy and credentials, in the protected machine data directory. Each enrollment gets its own credential for one user, client, agent, and profile, and the gateway refuses a request whose client, agent, or profile differs from the enrollment.
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 credential first, so a copied or cached token stops working even if the user-side cleanup is interrupted. The user's private copy is written as that user. Enrolling again after a revocation issues a new credential.
The managed Windows payload, the macOS managed runtime and the Linux deb, rpm
and payload archive include defenseclaw-acp as a separately hashed and
protected executable (/opt/defenseclaw/bin/defenseclaw-acp on Linux).
| 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 | Every enrollment record valid, and the binding configured |
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 |
In action mode a blocked prompt ends that turn normally: the editor shows the
reason as an agent message, in the wording the hook connectors use
("DefenseClaw policy blocked this action (rule SEC-AWS-KEY: AWS access key).
Do not retry it in another form."), and the next prompt runs. Agent output
blocked in the middle of a turn ends the turn the same way, and the guard
cancels the turn in the agent. The guard can only withhold what the agent
sends: an agent that runs its own tools has already run a step by the time
its tool_call update arrives, so an output block says "DefenseClaw policy
withheld agent output (rule ...). The agent may already have run the step;
check its effects." To stop such a step before it runs, keep the agent's own
connector hooks in action mode next to the guard. If the gateway does not answer an evaluation
(the guard retries once), only that step is refused, with "DefenseClaw could
not check this step because the gateway did not answer"; the session stays
open.
The guard starts the agent directly, never through a shell. It talks only to
the gateway on a loopback address, ignores proxy settings and redirects, and
checks that it is talking to the real gateway before it sends anything. Each
request is encrypted (AES-256-GCM) and each verdict is authenticated (HMAC),
so another process that takes the gateway's port sees neither the credential
nor the content and cannot fake a verdict; the guard fails closed instead. The
ACP credential works for the ACP routes only. 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.
An upgrade replaces the guard, and the installer then re-pins the new guard in
every user-owned lock that pinned the guard it replaced (defenseclaw acp refresh --from-sha256 <old digest>); a rolled-back upgrade restores the old
locks with the rest of the data. 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 recorded as a guardrail.evaluation.completed
event with these ACP attributes:
| 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.
A decision with findings (a block, an alert, or an observe-mode would-block)
and a profile-denied method are also exported as a trace: an
invoke_agent <agent> span (for example invoke_agent kiro, outcome
blocked for a block) with an apply_guardrail child that carries the
decision and rule IDs. Tempo shows both. Galileo has no guardrail span type,
so it records the invoke_agent span, whose metadata names the user and the
blocked outcome. The event, the finding rows and both spans carry the ACP
sessionId as the session and conversation ID and the trace and span IDs of
the decision. On a per-user gateway the finding rows and both spans also name
the user the gateway runs as (user.id, defenseclaw.user.name). A
rule-pack rule keeps its own ID in rule_ids.
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.
Supported agents
DefenseClaw ships its own list of ACP clients and agents, kept separately
from connector hooks. For each agent it records whether the entry point is
native or a bridge, where it is documented, and its ID in the upstream
ACP Registry when it has one.
defenseclaw acp catalog prints the list.
Kiro's native hooks are a separate, second enforcement path; see Kiro. The ACP guard needs none of that and covers every surface in the table above on its own. ACP adds no secret environment variable: agents keep their own credentials, and the guard token is a private file named on the guard's command line.
| 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.
A listed entry point was checked against the vendor's documentation or the ACP Registry; that alone does not mean DefenseClaw tested that editor and agent together.
Setup uses only the list that ships with DefenseClaw. It never downloads or runs commands from the online registry. 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 |
In managed mode, scoped_token_ready is true only when every enrollment
record is private and valid; one bad record makes it false.
Use defenseclaw acp catalog for the local catalog and
defenseclaw acp remove --client zed --agent kiro for surgical removal.
Legacy sandbox cleanup
Remove a retired openshell-sandbox 0.0.x standalone install from a Linux host with defenseclaw sandbox legacy-cleanup, before you set up OpenShell 0.1 sandboxes.
Human-in-the-Loop (HITL)
How DefenseClaw turns confirmable findings into native approval prompts when a connector can pause, and what happens on connectors without an ask surface.