Skip to content
Agentic Control Plane

Rules that stop the scary things

Six things people are afraid a coding agent will do, and the rule for each. Every recipe gives the same four answers: what the safety floor already does with no rule at all, the local rule for ~/.acp/policy.json (--local installs), the slash command you type in your agent (connected workspaces), and the console click. A rule is Allow, Ask first, or Never.

If you run agents with nobody watching, start with recipe 5. It is the setup we recommend: unattended tiers on Never for the dangerous keys, interactive sessions on Ask first, and production credentials kept out of the agent’s environment.

How a rule is keyed

ACP names every call with a dotted key: the tool, then the command, then the subcommand or host where that helps. rm -rf build is Bash.rm; git push origin main is Bash.git.push; curl https://api.github.com/... is Bash.curl.api.github.com; a file edit is Write or Edit. A rule on a shorter key covers everything under it, and the most specific key wins: Bash.curl.api.github.com: allow beside Bash.curl: ask lets GitHub calls through and asks about every other host.

There is no key for a flag, so --force is not a key. The safety floor is what tells a force-push from a plain push.

Local rules live in ~/.acp/policy.json, which the installer seeds in --local mode:

{
  "default": "allow",
  "rules": {
    "Bash.rm": "ask",
    "Bash.curl": "ask",
    "Bash.git.push": "ask",
    "Bash.chmod": "ask",
    "Bash.chown": "ask"
  }
}

Values are allow, ask, or deny. default applies when no key matches. A chained command (a && b | c) is split into segments and the strictest verdict wins: deny over ask over allow. Edit the file and the next call uses it; there is no mode switch in local mode. Local rules apply to every session the same way; there are no agent tiers in the local file.

Connected workspaces take rules three ways, and all three write the same rule:

  • Slash command, in your agent: /acp-ask Bash.git.push, /acp-deny Bash.rm, /acp-allow Bash.npm. The hook files it and prints a link you confirm with one tap. These write a rule for interactive sessions; rules for unattended tiers are set in the console.
  • Console: Policies, Tools tab, find or add the key, pick Ask first or Never. Or from any row on Activity: Ask next time / Deny next time.
  • From the agent: when it hits a hold on something legitimate it can propose a rule; proposals wait on Policies until you confirm.

A connected workspace starts in audit mode: everything is recorded, the safety floor applies, and your rules do not block yet. /acp-enforce (or the Policies page) turns them on. The recipes below assume enforce is on.

Where you approve a held call. Only Claude Code shows a safety-floor hold as a prompt in the session (workspace owner or admin at the keyboard). On every other harness, and for holds from your own rules on every harness, the call is blocked in the agent and waits on the console Approvals page and in the approval email. Approve it, then ask the agent to retry.

1. Force-push over shared history

Safety floor: a force-push (git push --force, -f, or a +refspec) is held for your approval in every mode, with or without a rule. A rule can tighten that to never; no rule can loosen it. A workspace owner or admin can switch the whole floor off on the Policies page (you were asked at install). --force-with-lease is not a force-push to the floor. Also held: git push --mirror, and deleting a protected branch or a tag on the remote (git push origin :main, git push --delete origin main). Deleting any other remote branch is recorded.

Local: make every push ask, or make it a never:

"Bash.git.push": "ask"
"Bash.git.push": "deny"

Slash: /acp-ask Bash.git.push or /acp-deny Bash.git.push.

Console: Policies → Tools → Bash.git.push → Ask first or Never.

What this does not do: there is no --force-only key, so a deny on Bash.git.push stops plain pushes too. If you want plain pushes through and force-pushes held, the floor already does exactly that with no rule. With --local the hook applies the same force-push hold on-device.

2. Writes or drops against the production database

Safety floor: destructive SQL handed to a database client (psql, mysql, mariadb, sqlite3) is held for your approval in every mode: DROP TABLE / DROP DATABASE, TRUNCATE, and DELETE FROM without a WHERE. It reads the SQL as a -c flag, a positional argument, a heredoc, or a piped literal; a script file passed with -f is not read. Not on the floor: ORM and CLI resets such as rails db:drop, prisma db push --force-reset, dropdb, or flyway clean. Those are recorded, and a rule on the key (for example Bash.prisma) is how you hold them.

Local: ask before any database client call:

"Bash.psql": "ask",
"Bash.mysql": "ask"

Slash: /acp-ask Bash.psql.

Console: Policies → Tools → Bash.psql → Ask first. For agents that run with nobody watching, Policies → Agents → background → Bash.psql → Never.

What this does not do: rules key on the tool, not the connection string, so ACP cannot tell prod-db.internal from localhost today. There is no per-host rule for database clients (the Patterns tab on Policies defines custom PII patterns for tool output, not command patterns). Your options are: ask before every client call in interactive sessions, make it a never in unattended tiers, or keep production credentials out of the agent’s environment.

3. rm -rf on the wrong directory

Safety floor: a recursive delete of your home directory, the root filesystem, or a system directory (/etc, /usr, /var, /Library, …) is always blocked, in every mode, and no approval reaches it. A recursive delete of a path outside the working directory (an absolute path, ~/..., ../...) is held for your approval in every mode. Deletes inside the project are the everyday case and pass to your rules. With --local the always-blocked list is narrower: home and root (not the system directories), mkfs, dd onto a disk, fork bombs.

Not on the floor: a delete inside the project (rm -rf src, git clean -fdx, git reset --hard) and a variable path such as rm -rf $DIR. Those are recorded, not held; Bash.rm: ask is the rule that holds them.

Local: ask before every rm (the seeded default), or make it a never:

"Bash.rm": "ask"
"Bash.rm": "deny"

Slash: /acp-ask Bash.rm or /acp-deny Bash.rm. In a connected workspace the starter rule already asks before rm once enforce is on.

Console: Policies → Tools → Bash.rm → Ask first or Never.

4. A secret leaving in a curl

Safety floor: none by default. ACP has a read-and-send detector (a sensitive file such as ~/.ssh/id_rsa, .env, or ~/.aws/credentials named in the same command as curl, wget, scp, or nc), but it ships off and has no console switch. Do not count on it today.

What you can do: ask before every outbound curl, and allow the hosts you trust by name. The most specific key wins, so this asks about every host except GitHub’s API:

Local

"Bash.curl": "ask",
"Bash.curl.api.github.com": "allow"

Slash: /acp-ask Bash.curl, then /acp-allow Bash.curl.api.github.com for each host you trust. The same shape works for Bash.wget and WebFetch.<host>.

Console: Policies → Tools → Bash.curl → Ask first; add Bash.curl.<host> → Allow for each trusted host.

Separately: a credential in a recognised format (GitHub, GitLab, Anthropic, OpenRouter tokens, private keys, passwords in URLs, Slack webhooks, password=-style assignments) that appears in a command’s arguments is redacted from the stored audit preview. The matcher is built for precision, so an unfamiliar token shape can get through. Redacting secrets out of tool output before the model sees them is a policy you turn on (output scanning starts in audit, recording what it would have redacted). Neither protects the outbound request; the rule above is what does.

5. An overnight or unattended run

The fear is two-part: it does something irreversible with nobody there to say no, and you cannot tell afterwards what it did.

Safety floor: the same floor applies with nobody watching. Always-blocked stays blocked. A held call goes to the Approvals page and the workspace owner’s email, and an unanswered request expires and counts as denied.

If ACP itself is unreachable, what happens depends on which hook is running. The Claude Code plugin keeps unattended tiers (subagent, background, api) blocked until the gateway answers; only interactive sessions proceed, with a loud UNGOVERNED notice, and the safety floor still runs on the machine either way. The shared hook that other harnesses use fails open in every tier unless you opt in: echo closed > ~/.acp/failmode (or set ACP_FAIL_MODE=closed) on that machine.

Console: rules by tier are the tool for this. Policies → Agents → background (and subagent): set Bash.rm, Bash.git.push, Bash.curl, and your database clients to Never, and leave interactive sessions on Ask first. Claude Code sessions started with --dangerously-skip-permissions are reported as background, as are CI and headless runs; a Task-spawned subagent as subagent; auto mode counts as interactive with the plugin (the bundled fallback hook reports it as subagent). Afterwards, the Sessions view (toggle at the top of Activity, or /sessions) shows the run call by call, with what each call cost.

Slash: slash commands write interactive-tier rules only, so the overnight rules are a console job.

Local: ~/.acp/policy.json has no tiers; a rule applies to every session. A deny there holds overnight too, and tail -f ~/.acp/audit.jsonl is the record. An ask in local mode needs someone at the terminal; with nobody there the harness’s own timeout decides.

6. A cost cap

Safety floor: none. Cost is not a rule; it is a limit.

Console: Settings → Limits → Spending Budget: a maximum spend per period, and the option to halt tool calls when it is reached rather than only warn. The cap counts model spend ACP can see. Claude Code’s model turns are priced from the session transcript by the hook, so plain claude counts. On other harnesses launch through the installer’s launcher (codex-acp, opencode-acp, …); a plain launch there records tool calls but not the model spend behind them.

Slash and local: no slash command sets a budget, and --local has no meter, so there is no local cost cap. Connected workspace only.

Questions people ask before adding rules

Why not auto mode plus a sandbox? Keep both. A sandbox limits where the agent can write and what it can reach, and auto mode decides per call. Neither tells a force-push from a plain push, knows that DROP TABLE is headed for a database client, or gives you one set of rules that applies by tier across every harness on the machine. ACP adds that layer and one log of what each call did.

Does it cover my other harness? The keys and rules are the same on every harness the installer sets up. What differs is how a hold is shown: only Claude Code shows it in the session; everywhere else the call is blocked and you approve it on the console or from the email, then the agent retries. Enforcement depth also differs by harness; check the setup page for yours.

What leaves my machine? In a connected workspace the hook sends each tool call to the gateway: the tool name, its input (the command or file path), and up to 200KB of its output. Recognised credential formats are redacted before a preview is stored. With --local, nothing is sent; decisions are made from ~/.acp/policy.json and recorded in ~/.acp/audit.jsonl on the same machine.

What if ACP is down? See recipe 5: the Claude Code plugin blocks unattended tiers and lets interactive sessions through with a loud notice; the shared hook fails open unless you set ~/.acp/failmode to closed. With the Claude Code plugin, the safety floor is checked on the machine and still applies while the gateway is unreachable.

  • Rules and scopes: how the three rule layers combine and why the most restrictive one wins.
  • Approvals: what a held call looks like, the grant window, expiry.
  • Getting started: install, turn on your rules, see the first hold.

Running coding agents? ACP records each tool call its hook sees and makes the dangerous shell commands (a force-push, destructive SQL, curl | sh) wait for you. One command, free up to 5 agents:

macOS · Linux · WSL
curl -fsSL https://agenticcontrolplane.com/install.sh | bash
Windows · PowerShell
irm https://agenticcontrolplane.com/install.ps1 | iex

What the installer writes  ·  Getting started