Framework guides

How to add human approvals to Claude Managed Agents

Route Claude Managed Agents session actions into Contro1 with session correlation, idempotent approval requests, signed callbacks, and audit-ready logs.

Use Contro1 as the approval and audit layer for Claude Managed Agents when a session pauses on required tool confirmation or custom tool work.

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

  • Start with one approval request for each blocking event referenced by session.status_idle stop_reason.event_ids.
  • Use session_id as correlation_id and session_id:event_id in external_request_id.
  • Verify signed Contro1 callbacks before sending user.tool_confirmation or user.custom_tool_result back to Claude.
  • Log autonomous allowed actions, Claude response-event delivery, and dead letters when they need evidence.

What this integration does

Claude Managed Agents can run long-lived sessions where the action that matters appears after several model and tool turns. Contro1 should sit at the session action boundary: when the stream emits session.status_idle with stop_reason.type = requires_action, your bridge reads the blocking stop_reason.event_ids, creates one routed approval request per risky event, and lets the session continue only after a verified decision.

This is different from Claude Code. Claude Code blocks a local CLI tool through PermissionRequest hooks; Claude Managed Agents need a server-side session bridge that tracks session_id, action IDs, callback delivery, and continuation retries.

Setup

Install the Contro1 Python SDK in the bridge process that listens to Claude Managed Agents session events and receives Contro1 callbacks.

.env
CENTCOM_API_KEY=cc_live_your_key
CENTCOM_BASE_URL=https://api.contro1.com/api/centcom/v1
CENTCOM_WEBHOOK_SECRET=whsec_your_secret
PUBLIC_BASE_URL=https://your-bridge.example.com

Short example: ask for approval

approve_managed_action.py
request = centcom.create_request(
    type="approval",
    question=f"Allow Claude managed-agent event {event_id}?",
    context={"event": blocking_event},
    callback_url=f"{PUBLIC_BASE_URL}/webhooks/contro1",
    required_role="developer",
    external_request_id=f"claude:{session_id}:{event_id}",
    correlation_id=session_id,
    metadata={
        "integration": "claude-managed-agents",
        "session_id": session_id,
        "blocking_event_id": event_id,
        "blocking_event_type": blocking_event["type"],
        "tool_name": blocking_event.get("name"),
    },
)

Full example: bridge action to approval

A production bridge should stream events, detect session.status_idle with stop_reason.type = requires_action, create one request for each blocking event ID, persist the mapping, wait for a signed callback, and then send the matching Claude event.

session_event_bridge.py
with anthropic_client.beta.sessions.events.stream(session_id) as stream:
    for event in stream:
        events_by_id[event.id] = event

        if event.type != "session.status_idle" or not event.stop_reason:
            continue
        if event.stop_reason.type != "requires_action":
            continue

        for event_id in event.stop_reason.event_ids:
            blocking_event = events_by_id[event_id]
            dedupe_key = f"claude:{session_id}:{event_id}"
            request = centcom.create_request(
                type="approval",
                question=f"Allow Claude event {event_id}?",
                context={"event": blocking_event},
                callback_url="https://bridge.example.com/webhooks/contro1",
                required_role="developer",
                external_request_id=dedupe_key,
                correlation_id=session_id,
                metadata={
                    "integration": "claude-managed-agents",
                    "session_id": session_id,
                    "blocking_event_id": event_id,
                    "blocking_event_type": blocking_event["type"],
                },
            )
            store_mapping(request["id"], session_id, event_id, blocking_event["type"])

Send the right Claude response event

Tool confirmations and custom tools resume differently. After Contro1 approves a tool-use confirmation, send user.tool_confirmation with the blocking event ID in tool_use_id and result = allow. If Contro1 denies it, send result = deny and include a deny_message.

For agent.custom_tool_use, do not send user.tool_confirmation. Execute the custom tool only after approval, then return user.custom_tool_result with custom_tool_use_id set to the blocking event ID.

send_tool_confirmation.py
anthropic_client.beta.sessions.events.send(
    session_id,
    events=[{
        "type": "user.tool_confirmation",
        "tool_use_id": event_id,
        "result": "allow" if approved else "deny",
        **({} if approved else {"deny_message": "Denied in Contro1"}),
    }],
)
send_custom_tool_result.py
result = execute_custom_tool(blocking_event["name"], blocking_event["input"])

anthropic_client.beta.sessions.events.send(
    session_id,
    events=[{
        "type": "user.custom_tool_result",
        "custom_tool_use_id": event_id,
        "content": [{"type": "text", "text": result}],
    }],
)

If routing fails, check who is available

Most integrations do not need Control Map in the normal approval path. If a request cannot be routed, times out unexpectedly, or your bridge wants to show a clear operational error, call Control Map to see who is currently available.

It helps answer which required roles are mapped, who is on shift now, whether fallback reviewers exist, and which warning explains the routing problem.

control_map.py
preview = centcom.post("/requests/control-map", {
    "approval_requirements": {
        "required_roles": ["developer"],
        "required_approvals": 1,
    },
    "metadata": {
        "integration": "claude-managed-agents",
        "session_id": session_id,
        "blocking_event_type": blocking_event_type,
    },
})

print(preview["satisfiable"])            # can this request be routed?
print(preview.get("on_shift_capacity"))  # who is currently available?
print(preview.get("fallback_reviewers")) # who can receive fallback routing?
print(preview.get("warnings"))           # why routing may fail

Callback and continuation rules

  • Verify X-CentCom-Signature and X-CentCom-Timestamp before reading the decision.
  • For agent.tool_use and agent.mcp_tool_use, answer with user.tool_confirmation.
  • For agent.custom_tool_use, run the custom tool only after approval, then answer with user.custom_tool_result.
  • Treat rejected, cancelled, timed_out, invalid signatures, and unknown request IDs as fail-closed.
  • Retry event delivery with bounded backoff, then write a dead-letter record if delivery is exhausted.

Webhooks · Audit records and cases · Claude Code approval hooks

Log autonomous and delivery actions

Every approval request already stores the human decision in Contro1. Use audit records for autonomous allowed actions, Claude response delivery, and dead-letter records.

log_allowed_action.py
centcom.log_action(
    action="claude_managed_agent.read_only_action_allowed",
    summary="Allowed read-only managed-agent event without human approval",
    source={"integration": "claude-managed-agents", "run_id": event_id},
    outcome="success",
    severity="info",
    correlation_id=session_id,
)
log_claude_event.py
centcom.log_action(
    action="claude_managed_agent.response_event_delivered",
    summary=f"Sent Claude response event for {event_id}",
    source={"integration": "claude-managed-agents", "workflow_id": blocking_event_type, "run_id": event_id},
    outcome="success",
    correlation_id=session_id,
    in_reply_to={"type": "request", "id": request_id},
)

Get evidence for compliance or incident review

Use request evidence for one reviewed managed-agent event. Use the case timeline when you need the full session: approvals, logs, denied events, response delivery, and dead letters sharing the same correlation_id.

get_evidence.py
evidence = centcom.get(f"/requests/{request_id}/evidence")
get_case.py
timeline = centcom.get(f"/cases/{session_id}")

Send full agent traceability

Beyond the approval call, attach identity, a run trace, the tools you invoked, and the context you retrieved. Each field is optional — add what you have. The verified identity always comes from your API key; a caller-supplied actor.agent_id is recorded as a claimed sub-agent until an admin verifies it.

  • trace_id / parent_trace_id — link one run (and sub-agent runs) into a single trace.
  • tool_calls[] — what the agent tried to do, so reviewers see the actions.
  • retrieved_context[] — the data the decision was based on (RAG provenance).
  • Then export a signed evidence packet from GET /requests/:id/evidence.
Send full traceability
# Same approval call you already make — now with full traceability.
client.requests.create(
    request_type="approval",
    title="Refund $4,200 to customer 8831",
    source={"integration": "claude"},
    actor={"agent_id": "billing-agent", "agent_name": "Billing Agent"},  # claimed sub-agent
    trace_id=f"trc_{run_id}",          # link every step of this run
    tool_calls=[{"name": "lookup_order", "outcome": "success"}],
    retrieved_context=[{"source": "policy:refunds", "uri": "kb://policy/refunds"}],
    continuation={"mode": "decision"},
)

Agent identity, traceability & signed evidence

Frequently asked questions

What should correlation_id be?

Use the Claude Managed Agents session_id. This groups every approval, callback, Claude response event log, and dead-letter record from the session into one Contro1 case timeline.

What should external_request_id be?

Use a deterministic key that includes session_id and the blocking event_id from stop_reason.event_ids. That makes retries and replayed status_idle events idempotent.

Do I send tool_confirmation for every approval?

No. Send user.tool_confirmation for agent.tool_use and agent.mcp_tool_use confirmations. For agent.custom_tool_use, execute the custom tool only after approval and send user.custom_tool_result.