# Rules that stop the scary things — ACP

Copy-paste ACP rules for force-push, prod database writes, rm -rf, a secret leaving in a curl, overnight runs, and a cost cap. For each: what the safety floor already does, the ~/.acp/policy.json snippet, the slash command, and the console click.

# 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](#5-an-overnight-or-unattended-run). 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:

```json
{
  "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](https://cloud.agenticcontrolplane.com/policies), **Tools** tab, find or add the key, pick Ask first or Never. Or from any row on [Activity](https://cloud.agenticcontrolplane.com/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](https://cloud.agenticcontrolplane.com/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:

```json
"Bash.git.push": "ask"
```

```json
"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:

```json
"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:

```json
"Bash.rm": "ask"
```

```json
"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**

```json
"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](https://cloud.agenticcontrolplane.com/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](https://cloud.agenticcontrolplane.com/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](https://cloud.agenticcontrolplane.com/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](/docs/setup) 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.

## Related

- [Rules and scopes](/docs/policies): how the three rule layers combine and why the most restrictive one wins.
- [Approvals](/docs/approvals): what a held call looks like, the grant window, expiry.
- [Getting started](/getting-started): install, turn on your rules, see the first hold.
