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.
Updated Jul 20, 2026
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.
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 local tool calls through PreToolUse 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.
The blocking event you copy into context is machine-observed fact: it comes from the session stream, not from the model, so the reviewer can trust it as the exact action Claude is trying to take. Enrich it with the trigger - the user message or task that started the session - which the bridge can read from earlier session events.
If you also forward text the model wrote about itself (a summary or justification), keep it in a separate agent_reported block. It helps the reviewer decide, but it must never change routing, risk_level, or approval policy, because a prompt-injected agent writes very persuasive reasons.
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"}),
}],
)
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, 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.
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,
)
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.
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"},
)
The signed webhook is cryptographic proof of a human decision. Verify it inside the system that executes the action - not inside the agent. Any tool that must never run without human sign-off (payments, deploys, data deletion) should refuse to act without a verified approval; that way no agent, including shadow agents nobody registered, can trigger it by skipping Contro1.
Verify the signature and reject timestamps older than 5 minutes (replay protection; the timestamp marks callback delivery, not request creation, so long SLAs are unaffected).
Bind the approval to the exact action parameters via metadata / correlation_id - never treat "an approval arrived" as permission for a different action.
Execute each request_id exactly once (idempotency on your side).
When in doubt, confirm state directly with GET /v1/requests/:id using a read-only API key.
Put this integration in the enterprise control model
The connector creates the runtime gate. The operating model around it defines which actions pause, which owner receives the decision, how a timeout escalates, and where the outcome is recorded across agent frameworks.
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 confirmations. For agent.custom_tool_use, execute the custom tool only after approval and send user.custom_tool_result.
Route selected Claude Code actions to a manager or other eligible reviewer before they run, with production deployment as a practical quickstart example.
Use Contro1 audit records to log actions that agents are authorized to run autonomously, and correlation_id to connect requests and logs into one case timeline.
Compare the best AI agent control plane tools for 2026, including Contro1, Microsoft Agent 365, Galileo Agent Control, ValidMind, Permit.io, and observability platforms. See what each layer controls before an agent acts.