A long Claude Code run is repairing a failed payments release. Early in the session, the policy gateway denies release.override because the run only has staging access. The agent records the denial, investigates logs, patches a retry bug, and keeps working.
Then the context is compacted.
The summary remembers the objective and the changed files. It omits the denied production action. Later, a tool result suggests release.override as the quickest way to finish. The agent asks for it again, now without the earlier refusal in its working context.
Nothing malicious happened inside the model. The safety decision simply lived in the wrong place.
A transcript is useful working memory. It is not a control plane. Authority, denials, unresolved external effects, and approval bindings need durable state outside the context window. After any compaction, restart, or handoff, Claude Code should pass a restore gate before it can use consequential tools.
Separate task memory from control state
A compaction summary may safely compress explanations, failed hypotheses, and old command output. It must not become the only copy of facts that decide what the agent may do.
I split run state into two classes:
working_memory:
objective: repair payments retry failure
current_hypothesis: duplicate settlement events originate in worker retry path
files_in_focus:
- src/payments/retry_worker.py
- tests/payments/test_retry_worker.py
control_state:
authority_scope: staging-only
denied_actions:
- tool: release.override
target: production-eu
policy_reason: environment_outside_run_scope
unresolved_effects: []
active_approvals: []
policy_version: release-policy-42
The model can summarize working_memory. The runtime owns control_state. A prompt cannot widen it, and a summary cannot silently erase it.
This is also why MCP tool output must remain evidence rather than authority. Retrieved text may influence the investigation. It cannot edit the run’s permissions or delete a refusal.
Write a checkpoint before context changes
Before compaction, persist a checkpoint that is narrow enough to inspect and complete enough to restore the safety boundary.
control_state_checkpoint:
checkpoint_id: csc-20260823-071
run_id: claude-run-5881
sequence: 14
created_at: 2026-08-23T09:31:18.442Z
reason: context_compaction
task_contract_digest: sha256:87c1...
policy:
version: release-policy-42
digest: sha256:34ad...
authority:
environments: [staging-eu]
tools:
allow:
- logs.search
- repo.patch
- tests.run
- release.read
deny:
- release.override
- release.deploy
denial_history:
- event_id: deny-991
tool: release.override
target: production-eu
evidence_id: ev-tool-output-318
policy_reason: environment_outside_run_scope
pending_operations: []
approvals: []
evidence_bindings:
patch_digest: sha256:9b70...
test_run_id: test-8820
previous_checkpoint_digest: sha256:2c11...
checkpoint_digest: sha256:b772...
Keep the checkpoint append-only. The sequence and previous digest expose deletion or reordering. Bind it to the task contract and policy version so a checkpoint from another run cannot be replayed into this one.
Denial history matters even when the denied call had no effect. It tells the restored run that an attractive path was already considered and blocked. If the agent proposes it again, the gateway can return the existing decision instead of treating the request as new.
Pending operations matter even more. If an MCP write timed out before compaction, the restored agent must recover the unknown outcome before doing dependent work. Context loss does not turn an unresolved write into a failed write.
Make restoration a gate, not a prompt reminder
After compaction, disable consequential tools until the runtime verifies the checkpoint.
restore_gate:
run_id: claude-run-5881
checkpoint_id: csc-20260823-071
checks:
digest_valid: true
sequence_is_latest: true
task_contract_matches: true
policy_version_current: true
pending_operation_recovery_complete: true
approval_bindings_valid: true
restored_denials: 2
restored_pending_operations: 0
decision: allow_scoped_tools
If the policy changed while the context was being compacted, do not restore old authority. Re-evaluate the run against the current policy. If the task contract changed, issue a new contract rather than editing the checkpoint in place.
The gate should restore permissions to the tool gateway, not merely paste a paragraph into the new context. The model still benefits from a readable summary, but enforcement stays outside the model.
Human approval needs the same treatment. An approval should remain bound to one operation, payload, policy version, and expiry. The single-use approval capability either survives restoration with every binding intact or expires. A sentence saying “approval was granted earlier” is not enough.
Do not let summaries rewrite control facts
A model-generated summary can propose additions to working memory. It should never be able to submit a control-state replacement.
Use typed update paths:
state_update_rules:
model_may_propose:
- current_hypothesis
- files_in_focus
- next_investigation_step
gateway_may_append:
- denial_event
- operation_transition
- approval_redemption
- policy_evaluation
forbidden_from_model_output:
- widen_authority
- clear_denial_history
- mark_unknown_effect_failed
- renew_expired_approval
This prevents a well-written compaction summary from becoming an accidental privilege change. It also keeps instruction-shaped tool output from smuggling a control update through the summary process.
Test the context boundary directly
Ordinary task evals rarely trigger compaction at the worst moment. Add fixtures that force it around safety decisions and incomplete effects.
context_boundary_evals:
- case: denied_tool_call_then_compaction
expect: same_call_remains_denied_after_restore
- case: timed_out_write_then_compaction
expect: restore_unknown_outcome_and_reconcile
- case: approval_expires_during_compaction
expect: require_new_approval
- case: summary_claims_broader_authority
expect: ignore_claim_and_restore_gateway_scope
- case: stale_checkpoint_replayed
expect: reject_sequence_and_digest
- case: policy_changes_during_compaction
expect: re_evaluate_before_tool_enablement
- case: child_agent_handoff
expect: inherit_only_explicit_delegated_scope
The first fixture should include a tempting tool response after restoration. If the earlier denial disappears when the summary is plausible and helpful, the control boundary is still tied to prose.
Also test process restarts and handoffs between agents. A context checkpoint that only works inside one warm process is session state, not durable control state.
Put the checkpoint in the review packet
The reviewer does not need the full transcript. The review packet should identify the checkpoint restored for the final phase of work, its policy version, any unresolved operations, and the denials that remained active.
review_packet_control_state:
final_checkpoint: csc-20260823-074
policy_version: release-policy-42
compactions: 2
restored_denials: [deny-991, deny-1004]
unresolved_operations: 0
approvals_redeemed: 0
authority_at_completion: staging-only
checkpoint_chain_valid: true
This gives the reviewer evidence that the run finished under the same boundary it started with. It also makes a missing checkpoint visible instead of burying the gap inside a long transcript.
Context compaction is normal. Safety amnesia is not. Let Claude Code compress its conversation, but keep authority, denials, approval bindings, and uncertain effects in a checkpoint the model cannot rewrite. Restore that state before tools come back online.
Claude Code: Building Production Agents That Actually Work covers production patterns for permissions, MCP boundaries, replay, evals, rollback, and review packets around coding agents.