Skip to content
Agentic Control Plane

Governance for Claude Code in 60 seconds

David Crowe · · 5 min read
claude-code governance tutorial hooks

Claude Code is an autonomous agent that reads your files, writes code, runs shell commands, and makes web requests. It’s powerful. 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

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

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. Opens your browser to log in and provision a workspace

After a restart (claude --continue), every tool call flows through the hook.


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:

// ~/.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:

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 API response. Calls that violate your policies are denied before the tool executes. If ACP is unreachable, the hook fails closed — nothing runs ungoverned.

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

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

Takes 30 seconds. Free. Every Claude Code tool call, governed.

View the dashboard →

Share: Twitter LinkedIn
Related posts

← back to blog