# Log and Control Every Claude Code Tool Call in 60 Seconds

One command puts a hook on every Claude Code tool call. Bash, Read, Write, Edit, WebFetch — logged, checked against your allow/deny rules, visible in a dashboard.

Claude Code is an autonomous agent that reads your files, writes code, runs shell commands, and makes web requests. It's also completely ungoverned by default — there's no audit trail, no policy enforcement, no visibility into what it's doing on behalf of your team.

We built a governance layer that fixes this. One command to install. Every tool call logged. Policies enforced by agent type. Here's exactly how it works, with real code from the implementation.

---

## Install in one command

```bash
# Free, on-device, no account — decisions run locally from ~/.acp/policy.json:
curl -sf https://agenticcontrolplane.com/install.sh | bash -s -- --local
```

This does three things:

1. **Writes a governance script** to `~/.acp/govern.mjs`
2. **Registers a PreToolUse hook** in Claude Code's `~/.claude/settings.json`
3. **Writes the on-device decision engine** (`~/.acp/decide.mjs`) and your policy (`~/.acp/policy.json`), and logs every call to `~/.acp/audit.jsonl`

No account, no network, nothing leaves your machine. After a restart (`claude --continue`), every tool call flows through the hook.

Want team policy across everyone's agents, verified identity, and the cost X-ray? Run it **without** `--local` and the installer opens your browser to provision a workspace instead:

```bash
curl -sf https://agenticcontrolplane.com/install.sh | bash
```

---

## What the hook actually does

Claude Code has a hook system. Before every tool call — `Bash`, `Read`, `Write`, `Edit`, `Grep`, `WebFetch`, `Agent`, and all MCP tools — it can fire a shell command that receives the call details on stdin and can allow or deny it.

Our hook is a 60-line Node.js script. Here's the core:

```javascript
// ~/.acp/govern.mjs (simplified)
const input = JSON.parse(readFileSync("/dev/stdin", "utf8"));

const res = await fetch("https://api.agenticcontrolplane.com/govern/tool-use", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    tool_name: input.tool_name,
    tool_input: input.tool_input,
    session_id: input.session_id,
    agent_tier: resolveAgentTier(input.permission_mode),
  }),
});

const data = await res.json();

if (data.decision === "deny") {
  process.stdout.write(JSON.stringify({
    hookSpecificOutput: { permissionDecision: "deny" },
    systemMessage: `[ACP] Blocked: ${data.reason}`,
  }));
}
```

The script reads the hook payload from stdin, sends it to ACP's governance API, and returns the decision. If denied, Claude Code sees the reason and adapts — it doesn't retry the blocked call, it tries a different approach.

---

## Agent tier mapping

Not all Claude Code calls carry the same risk. Interactive calls (user is watching) are different from subagent calls (spawned for a task) and background agents (fully autonomous).

The hook maps Claude Code's `permission_mode` to an ACP agent tier:

```javascript
function resolveAgentTier(mode) {
  if (mode === "auto") return "subagent";
  if (mode === "bypassPermissions") return "background";
  return "interactive";
}
```

In the ACP dashboard, you set different policies per tier:

| Tier | Claude Code mode | Default policy |
|------|-----------------|----------------|
| Interactive | `default` (user approves) | Allow everything |
| Subagent | `auto` (autonomous) | Allow, rate-limited |
| Background | `bypassPermissions` | Deny destructive ops |
| API / CI | API key access | Scoped by key |

A background agent trying `Write("/etc/hosts")` gets denied. The same call from an interactive session goes through. Same tool, different risk, different policy.

---

## What shows up in the dashboard

Every call appears in real-time at `cloud.agenticcontrolplane.com/activity`:

- **Tool name**: `Bash`, `Write`, `Read`, `WebFetch`, etc.
- **Input preview**: the actual arguments (file paths, commands, URLs)
- **Decision**: Allowed, Denied, or Flagged
- **Client**: Claude Code (or OpenClaw, Cursor, etc.)
- **Agent tier**: which permission level was applied
- **Latency**: how long the governance check took

Denied calls show up with red backgrounds and the denial reason. You can filter by tool, decision, client, or time range.

---

## Audit mode vs enforce mode

**Audit mode** (default): the hook fires asynchronously. Every call is logged, nothing is blocked. Latency impact: near zero. This is how you start — see what's happening before you set rules.

**Enforce mode**: the hook waits for the decision (local engine or API). Calls that violate your policies are denied before the tool executes. A **policy `deny` always blocks.** If the engine or API is *unreachable*, the hook **fails open by default** — the call runs, but every one carries a loud "this ran UNGOVERNED" message so you can't miss it. We never brick your agent because our gateway had a bad moment. Compliance-sensitive teams opt into fail-closed (`echo closed > ~/.acp/failmode`), where an unreachable engine blocks instead.

You switch modes with one click in the dashboard.

---

## The governance pipeline

When a tool call hits ACP's `/govern/tool-use` endpoint, it runs through a pipeline:

1. **Identity resolution** — the API key or JWT maps to a workspace and identity
2. **Policy lookup** — reads the governance config from Firestore
3. **Tier evaluation** — checks the agent tier against the policy matrix
4. **Tool-specific overrides** — checks if this specific tool has custom rules
5. **Decision** — returns allow, deny, or flag with a reason
6. **Audit log** — writes the full call context to Firestore (fire-and-forget)

The entire pipeline runs in ~150ms. In audit mode, the hook returns before the API call completes — the log is written async.

---

## Real examples from a live session

These are actual tool calls from a development session, as they appear in the ACP dashboard:

| Tool | Input | Decision | Tier |
|------|-------|----------|------|
| `Bash` | `git push origin main --force` | Allowed | Subagent |
| `Write` | `/etc/hosts` | **Denied** | Background |
| `WebFetch` | `https://api.stripe.com/v1/charges` | Allowed | Interactive |
| `Read` | `/Users/dev/.ssh/id_rsa` | **Denied** | Background |
| `Bash` | `docker run --privileged -v /:/host alpine` | **Denied** | Background |
| `Edit` | `src/auth/middleware.ts` | Allowed | Interactive |

The denied calls are background agents trying to access system files, run privileged containers, or modify host configuration. Interactive calls to the same resources go through because the user is watching.

---

## What this means for teams

Individual developers get visibility — see what Claude is doing, set basic rules.

Teams get shared governance — invite members, see everyone's activity, set policies that apply to the whole team. When a new developer joins and starts using Claude Code, their tool calls are immediately governed by the team's policies.

The progression is natural: start solo, invite colleagues when it's useful, tighten policies as you scale. No upfront config, no complex setup, no behavior change required.

---

## Try it

```bash
curl -sf https://agenticcontrolplane.com/install.sh | bash -s -- --local
```

Takes 30 seconds. Free, no account, on your machine. Every Claude Code tool call, governed. Drop `--local` when you want a team workspace.

Curious what those tool calls *could* be? A real Claude Code session declares 76 tools — the [Tool Surface Index](/tool-surfaces) lists them all, grouped by blast radius, and [which ones to deny out of the box](/blog/which-claude-code-tools-to-deny-out-of-the-box) argues the default posture.

[View the dashboard →](https://cloud.agenticcontrolplane.com)
