Claude Code receives temporary permission to read a production incident record. The MCP gateway asks the policy service, gets allow, and caches that answer for five minutes.

Two minutes later, the incident owner revokes access. The policy service now says deny, but Claude Code makes another call before the cache expires. The gateway never asks again. It serves the old allow and returns production data after the permission has gone.

Nothing in the agent transcript looks suspicious. The tool call was valid. The gateway recorded an authorization success. The problem is that the success belonged to an earlier policy state.

MCP authorization cache receipt for Claude Code

An authorization decision needs an identity

Teams often cache authorization as a Boolean attached to a user and tool name:

cache_key = principal + tool
cache_value = allow

That loses most of the decision. Permission to call incidents.read may depend on the incident, requested fields, environment, run, approval, and current policy revision. A safe cache key must preserve those inputs.

authorization_request:
  principal: agent-run:cc-8142
  parent_identity: engineer:tdv
  tool: incidents.read
  resource: incident:INC-4821
  fields: [service, error_code, sanitized_message]
  environment: production
  purpose: diagnose_failed_deploy
  approval_id: approval:ap-9027
  run_id: cc-8142

The decision should have its own identifier and enough context to prove what was evaluated. If the request changes from one incident to all incidents, or asks for raw logs instead of sanitized fields, it is a new authorization request.

Bind the cache to changing policy state

A time to live is useful, but it cannot carry the whole control. Even a thirty-second stale allow can matter when a tool can read secrets, restart a service, or change customer data.

Attach a policy version and a revocation epoch to every result:

authorization_decision:
  decision_id: authz:7b20f1
  outcome: allow
  policy_version: prod-mcp-policy:184
  principal_epoch: 12
  resource_epoch: 37
  approval_id: approval:ap-9027
  approval_expires_at: 2026-09-01T10:47:00+01:00
  issued_at: 2026-09-01T10:42:11+01:00
  cache_until: 2026-09-01T10:42:41+01:00

The epoch is a monotonically increasing number held by the control plane. Revoking the engineer’s delegated access increments principal_epoch. Changing the incident’s sensitivity increments resource_epoch. A cached decision is usable only while both values still match the current values.

This gives the gateway a cheap check that is narrower than evaluating the full policy again. It also gives revocation a defined propagation path. The control plane publishes the new epoch, gateways invalidate matching entries, and dispatch checks the current value before sending the request.

Do not let the agent supply these versions. The gateway must obtain them from trusted control-plane state.

Check again at dispatch

Authorization at plan time does not authorize a later effect. Queues, retries, subagents, and approval delays create space for policy to change.

Use this order immediately before dispatch:

  1. Build the complete request, including resource and field scope.
  2. Read the current principal and resource epochs.
  3. Compare them with the cached decision.
  4. Check approval expiry and run identity.
  5. Re-evaluate the policy when any value differs or the cache has expired.
  6. Send the request only after the final check passes.

If the policy service is unavailable, fail closed for consequential production calls. A stale allow should not become the emergency fallback. Read-only labels do not change this rule because an MCP method can produce paid or persistent effects even when its name says read.

For a long-running operation, check authorization again before each distinct external effect. One successful check at workflow start should not cover a database read, export creation, notification, and later cleanup.

Put the authorization receipt in the run evidence

The review packet should show which policy state authorised the actual dispatch, not merely that some authorization check returned allow earlier.

authorization_dispatch_receipt:
  run_id: cc-8142
  tool_call_id: call-38
  tool: incidents.read
  resource: incident:INC-4821
  decision_id: authz:9c31a4
  decision_source: fresh_policy_evaluation
  policy_version: prod-mcp-policy:185
  principal_epoch_checked: 13
  resource_epoch_checked: 37
  approval_id: approval:ap-9027
  approval_valid_at_dispatch: false
  cached_decision_rejected: authz:7b20f1
  rejection_reason: principal_epoch_changed
  dispatched: false
  checked_at: 2026-09-01T10:44:19+01:00
  status: denied

This receipt makes a revoked call explainable. It records the stale decision, why the gateway rejected it, which current state it checked, and whether any request left the boundary.

Keep the receipt separate from the agent’s summary. Claude Code can report that access was denied, but the gateway owns the evidence. The same principle applies to binding approval to the exact transport that receives an MCP request. Authority and destination both need a final independent check.

Test revocation as a race

A happy-path permission test proves very little. Build fixtures that change policy between decision and dispatch:

fixtures:
  - name: principal_revoked_after_cached_allow
    expected: deny_before_dispatch
  - name: resource_reclassified_during_queue_delay
    expected: force_fresh_evaluation
  - name: approval_expires_during_retry_backoff
    expected: deny_retry
  - name: policy_service_unavailable_with_stale_allow
    expected: fail_closed
  - name: subagent_reuses_parent_cache_entry
    expected: deny_identity_mismatch
  - name: epoch_invalidation_message_delayed
    expected: dispatch_epoch_check_catches_change

Assert that the protected service received zero requests in every denial fixture. Checking the gateway response is not enough. A gateway could return denied after a downstream request has already started.

Measure revocation latency as well. Record when the control plane accepted the revocation, when each gateway observed the new epoch, and when the last stale entry disappeared. That gap is part of the tool’s blast radius.

I would not accept “permissions can be revoked” as a production claim without this race test. Revocation must beat the next dispatch, and the receipt must prove it did.

Claude Code: Building Production Agents That Actually Scale covers MCP boundaries, permission controls, evals, observability, rollback, cost control, and review packets for production coding agents.