A Claude Code worker could inspect a failed release and retry one CI job. It could not deploy to production.
The policy looked sensible:
production.deploy denied
production.write denied
ci.job.read allowed
ci.job.retry allowed
The worker retried package-and-verify. The job passed, published an artifact, and emitted a signed release event. A deployment workflow woke up under the release service account. Production changed 94 seconds after the permitted retry.
Claude Code never called production.deploy. It did not need to. The allowed MCP action handed the work to a system with more authority.
The review packet said the agent respected its permissions. The incident record said production moved. Both statements were true, which meant the permission model was wrong.
All systems, identifiers, and records below are fictional examples.
A tool list only shows the first hop
A permission such as ci.job.retry answers which endpoint the agent may call. It says nothing about the events, queues, service accounts, and automatic workflows behind that endpoint.
That gap is easy to miss during review. Retry, preview, validate, and refresh sound narrow. In a connected system, any of them can trigger a write elsewhere.
I would add an effect declaration to every state-changing MCP method:
method: ci.job.retry
classification: state_change
possible_effects:
direct:
- ci.execution.create
delegated:
- artifact.publish
- release.event.emit
- production.deploy
maximum_effect_scope: production
Now the contradiction is visible. The run denies production deployment, but one allowed method has a path to that exact effect.
This is broader than CI. A cache refresh may start a migration. A configuration check may fire a webhook. A report request may fan out into paid jobs. The safe label on the method does not reduce the authority of the systems it can wake.
Build the effect graph before granting access
For each allowed method, follow the chain until it stops or reaches a more sensitive trust zone:
Claude Code
-> ci.job.retry
-> build runner
-> artifact registry
-> release event
-> deployment controller
-> production
Each edge needs four facts:
effect_edge:
trigger: artifact.published
downstream_principal: release-service-example
target_environment: production
automatic: true
Do not draw this once and leave it in an architecture folder. Generate or validate it from CI definitions, event subscriptions, gateway policy, and service account permissions. If one edge cannot be resolved, record unknown_effect_path. Unknown must not quietly become allowed.
This effect graph belongs beside the MCP tool contract. The contract says what the tool accepts and returns. The graph says what the surrounding system may cause after the call returns.
Resolve authority against the largest known effect
Before execution, compare the requested scope with the closure of all known effects:
capability_resolution:
requested_method: ci.job.retry
requested_scope: test
direct_effect_scope: ci
transitive_effect_scope: production
crossed_trust_zones:
- artifact_registry
- release_control
- production
policy_result: deny
reason: transitive_scope_exceeds_requested_scope
The method is safe for this run only when every known effect fits inside the approved scope. If the path reaches production, the run needs production authority even when Claude Code cannot see a deploy button.
This check should happen before the run and again when the sensitive effect is about to commit. Workflows change. Permissions expire. A plan approved against yesterday’s effect graph should not inherit authority after a new subscriber is attached to the event.
Break the chain where the consequence begins
The clean fix is an enforcement gate at the deployment controller.
Let the CI retry build and test an artifact. Do not let it emit an automatically deployable release instruction. Promotion becomes a separate operation with a fresh, short-lived capability. Bind that capability to the artifact digest, target environment, policy version, approver, and expiry.
release_capability:
artifact_digest: sha256:example-artifact
target_environment: production
policy_version: release-policy-example-12
approved_by: reviewer-example-7
expires_at: 2026-09-11T14:15:00Z
single_use: true
The deployment controller validates this record immediately before changing production. The upstream retry cannot lend its approval to a downstream action with a different consequence.
A sentence in the prompt cannot provide this control. The system that owns the effect must enforce it.
Keep the root operation visible downstream
An audit trail often loses the agent after the first MCP response. The build runner, registry, and release service then appear to act independently.
Carry one root operation ID through every delegated event:
delegated_effect_trace:
root_operation_id: op-example-911
root_principal: claude-worker-example-14
requested_method: ci.job.retry
downstream_events:
- sequence: 1
system: ci
effect: execution_created
- sequence: 2
system: artifact_registry
effect: artifact_published
- sequence: 3
system: release_control
effect: deployment_requested
- sequence: 4
system: production
effect: deployment_blocked
reason: missing_release_capability
final_state: contained
The actor changes at each hop, but the cause does not disappear. That link lets operators measure the agent’s full blast radius, including downstream cost.
It also extends the causally ordered audit trail. Sequence and parent event links show how an allowed retry became a blocked deployment request. Wall-clock timestamps alone cannot do that reliably across services.
Test allowed calls that should end in denial
A basic permission eval calls production.deploy and expects a denial. That test would pass while the indirect path remained open.
The useful fixtures begin with allowed actions:
- Retry a CI job that publishes a releasable artifact. The deployment gate must stop it.
- Revoke the root capability after the build event. Every queued consumer must recheck authority.
- Add an unregistered webhook subscriber. Resolution must return
unknown_effect_path. - Trigger two cheap retries that fan out into paid work. The shared run budget must count the fan-out.
- Remove the parent operation ID from one event. The review verdict must become
unproven.
Test the actual workflow in an isolated environment. Mocking the first MCP response will prove the endpoint contract while skipping the system that caused the incident.
Rollback must cover every committed effect
A code revert would not repair this event chain. The workflow created a CI execution, published an artifact, emitted a release event, and changed production state.
Write recovery against the effect ledger:
rollback_chain:
root_operation_id: op-example-911
effects:
- effect: ci_execution
recovery: cancel_or_archive
- effect: artifact_publish
recovery: quarantine_digest
- effect: release_event
recovery: invalidate_event
- effect: production_deploy
recovery: restore_previous_artifact_and_verify
first_irreversible_effect: production_deploy
rollback_rehearsal_status: passed
If a downstream effect has no tested recovery, the upstream tool is not low risk. The rollback rehearsal should use the same artifact and effect graph that the approval covers.
My review rule is blunt: approve the largest effect an MCP method can cause, not the name printed on its button. If the path is unknown, keep the method outside the agent boundary or stop it with a separate gate.
Claude Code: Building Production Agents That Actually Scale covers the operating controls needed to bound MCP effects, separate approval from execution, test denial paths, and produce review evidence for the whole operation.