Framework guides

How to route Claude Code permission requests into Contro1

Use the PermissionRequest hook to send risky Claude Code actions into Contro1 before allowing write, edit, or shell execution.

Claude Code is a useful example of runtime control because approvals happen at the exact tool boundary where risk becomes real.

Use the integration skill

Copy this skill link into your code agent to add Claude Managed Agents and Contro1 to your system.

Copy skill link

Key takeaways

  • PermissionRequest hooks intercept risky tools (Write, Edit, Bash) and route approval through Contro1.
  • For managed agents, session_event_bridge.py turns requires_action events into Contro1 requests.
  • Dead-letter exhausted retries instead of dropping them silently.
  • The agent's system prompt still matters: tell it which categories of actions are ALWAYS gated.

When to reach for Contro1 with Claude Code

Claude Code is interesting because approvals happen where they matter most - at the tool boundary, right before the action touches your filesystem, shell, or code. That's "runtime control" as opposed to prompt-level guardrails.

You have two integration paths. The CLI tool uses PermissionRequest hooks. Managed Agents use the session event stream (requires_action) - we ship a ready-made bridge for that case.

Installation

pip install
pip install centcom flask python-dotenv
.env
CENTCOM_API_KEY=cc_live_your_key
CENTCOM_BASE_URL=https://api.contro1.com/api/centcom/v1
CENTCOM_WEBHOOK_SECRET=whsec_your_signing_secret

Basic integration (CLI)

.claude/settings.json
{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "Write|Edit|MultiEdit|Bash|NotebookEdit",
        "hooks": [
          {
            "type": "command",
            "command": "centcom-claude-code",
            "timeout": 310
          }
        ]
      }
    ]
  }
}

Basic integration (Managed Agents)

session_event_bridge.py
from anthropic import ManagedAgentsClient
import centcom

client = ManagedAgentsClient()

for event in client.sessions.stream_events(session_id):
    if event.type != "requires_action":
        continue
    for action in event.required_actions:
        centcom.create_request(
            type="approval",
            question=f"Approve {action.type} call?",
            context=str(action.input),
            required_role="developer",
            callback_url="https://bridge.example.com/callback",
            external_request_id=f"claude:{session_id}:{action.id}",
            metadata={"session_id": session_id, "action_id": action.id},
        )

Case continuity

For Claude Code CLI, set CENTCOM_CORRELATION_ID to a stable session or project identifier so every tool approval in the same coding session appears in one case timeline. For managed agents, use session_id as correlation_id.

The request id stays tied to a single PermissionRequest or requires_action event; correlation_id ties the surrounding timeline together.

managed_case.py
request = client.create_protocol_request({
    "title": f"Managed agent action: {action_type}",
    "request_type": "review",
    "source": {"integration": "claude-managed-agents", "session_id": session_id, "run_id": external_action_id},
    "continuation": {"mode": "instruction", "callback_url": callback_url},
    "external_request_id": f"claude:{session_id}:{external_action_id}",
    "correlation_id": session_id,
})
permission_hook.ts
const request = await client.createProtocolRequest({
  title: `Approve Claude Code tool: ${toolName}`,
  request_type: 'approval',
  source: { integration: 'claude-code', workflow_id: 'permission-hook' },
  continuation: { mode: 'decision' },
  external_request_id: `claude-code:${toolCallId}`,
  correlation_id: process.env.CENTCOM_CORRELATION_ID, // set per session
});

Audit records and cases reference

Logging autonomous actions

Use logAction for Claude Code actions that the hook auto-allows, such as read-only commands or safe inspections. This keeps a durable audit trail without interrupting the developer.

When the log describes a follow-up after a human answer, include in_reply_to with the Contro1 request id.

audit_action.ts
await client.logAction({
  action: 'claude_code.command_allowed',
  summary: 'Allowed read-only command: rg "invoice"',
  source: { integration: 'claude-code', workflow_id: 'permission-hook' },
  outcome: 'success',
  correlation_id: process.env.CENTCOM_CORRELATION_ID,
  in_reply_to: { type: 'request', id: request.id },
});

Gate the tool before it executes

The tool function itself is the right place to require approval for irreversible actions. The first line of a destructive tool calls Contro1 and blocks until an operator decides. Nothing runs until the human says yes - no prompt engineering needed.

For the Claude Code CLI, the PermissionRequest hook shown above is already the tool gate - it fires before every Write, Edit, and Bash command and blocks until Contro1 resolves. No extra code needed.

For tools in a Managed Agents server, add the approval call as the first line of any destructive function:

tools/deploy.py
import os
import centcom

_centcom = centcom.Client(api_key=os.environ["CENTCOM_API_KEY"])

def run_deploy(version: str) -> dict:
    req = _centcom.create_request(
        type="approval",
        question=f"Deploy version {version} to production?",
        context={"version": version},
        required_role="developer",
        external_request_id=f"deploy:{version}",
    )
    decision = _centcom.wait_for_response(req["id"], timeout=600)
    if not (decision.get("response") or {}).get("approved"):
        raise PermissionError("Deploy rejected by operator")
    return ci.trigger_deploy(version)

Pause the agent on system error - orchestrator level

The managed-agents session stream occasionally emits error events. Treat them like requires_action: map them to a Contro1 request so an on-call developer decides whether to cancel the session, restart it, or continue with a workaround.

error_bridge.py
for event in client.sessions.stream_events(session_id):
    if event.type != "error":
        continue
    req = centcom.create_request(
        type="approval",
        question=f"Claude session {session_id} errored. Restart?",
        context=f"{event.error.code}: {event.error.message}",
        required_role="oncall",
        priority="urgent",
        external_request_id=f"claude-err:{session_id}:{event.id}",
    )
    decision = centcom.wait_for_response(req["id"], timeout=900)
    if decision["response"].get("approved"):
        client.sessions.restart(session_id)
    else:
        client.sessions.cancel(session_id)

Escalate tool errors to a human

If you ship your own tool implementations that Claude can call, wrap each tool so provider errors escalate to a human before returning to the model. This prevents the model from "trying to fix" an infrastructure problem it cannot fix.

tools/deploy.py
import centcom

def deploy_release(version: str) -> dict:
    try:
        return ci.trigger_deploy(version)
    except ci.RegistryUnavailable as exc:
        req = centcom.create_request(
            type="approval",
            question=f"Registry unavailable during deploy of {version}. Retry?",
            context=str(exc),
            required_role="oncall",
            external_request_id=f"deploy-registry:{version}",
        )
        decision = centcom.wait_for_response(req["id"], timeout=900)
        if decision["response"].get("approved"):
            return ci.trigger_deploy(version)
        return {"status": "deploy_blocked_by_operator"}

Prompt engineering: force the agent to pause

Even with PermissionRequest hooks, you want Claude itself to know the rules. This block is designed for the model's system message - it frames approval as policy, not friction.

CLAUDE.md
## Approval policy (Contro1)

The following actions are always gated by a human approval step. Do not attempt to bypass them, work around them, or rephrase them:

  - Any Write, Edit, or MultiEdit on files outside of the current working directory.
  - Any Bash command that includes: rm, git push, git reset --hard, curl -X POST against production hosts, deploy, terraform apply.
  - Any change to CI/CD configuration, infra-as-code, or .env files.

When a PermissionRequest is issued, explain what you intended to do in the "why" field in one sentence. The reviewer may approve with comments, reject, or modify. If the reviewer rejects, STOP. Report the rejection verbatim to the user. Do not re-attempt with a "safer" variation.

If the approval service is unreachable, STOP. Do not guess permission.

See our GitHub integration repo

Our managed-agents connector is the most battle-tested bridge in this doc set - it handles retries, dead-letter queues, and signature verification end to end.

centcom-claude-managed-agents on GitHub · session_event_bridge.py - production bridge · claude-managed-agents-connector.md - architectural guide

Frequently asked questions

Should I gate every Claude Code action?

No. Gate the actions that can change code, systems, or data in ways that matter to your organization. Read-only tools, searches, and plan-only steps should never be gated.

What is the difference between PermissionRequest hooks and the managed-agents bridge?

PermissionRequest is for the Claude Code CLI installed on a developer's machine. The managed-agents bridge is for cloud-hosted Anthropic agents consuming tools via the session event stream. Same approval semantics, different transport.

How does the managed-agents bridge handle retries?

Up to 4 attempts with exponential backoff on continuation failures. Exhausted failures move to a dead_letters table for manual replay rather than being dropped silently.

Can different matchers route to different approvers?

Yes. Multiple PermissionRequest hook blocks can specify different matchers and different commands, and each command can create Contro1 requests with different required_role values.

How do I audit every gated action later?

Every Contro1 request is stored with its question, context, operator, decision, and correlation metadata. You can export or stream them to your SIEM for long-term audit.