Claude Code starts a release task. One MCP call asks the deployment service for a preview. Another queues a database compatibility check. A subagent prepares a ticket update. Then the operator sees the wrong commit in the run and presses stop.

The model stops producing tokens. The queue does not care.

Thirty seconds later, the compatibility job finishes and publishes its result. The ticket update follows. The deployment preview reserves a shared environment. The run was stopped, yet its authority kept moving through systems that had already accepted work.

Killing the client process is not a production stop control. I want a cancellation barrier that revokes the run’s authority, rejects work that has not started, reconciles work already in flight, and proves that no later effect can still appear under the old run ID.

Claude Code MCP cancellation barrier

Separate four meanings of stopped

Teams often use one word for four different states:

stop_states:
  generation_stopped:
    meaning: model is no longer producing output
  dispatch_stopped:
    meaning: gateway will not accept new calls from the run
  queued_work_cancelled:
    meaning: accepted calls that have not started cannot execute
  effects_settled:
    meaning: in-flight calls are reconciled and no unknown effect remains

Only the last state is safe enough for an operator to hear “the run has stopped.”

A terminal interrupt may achieve the first state. Closing the session may eventually achieve the second. Neither tells you what a remote queue, CI worker, deployment controller, or SaaS API will do with work it already holds.

This becomes harder with subagents. A parent run can stop while a child process still has a valid lease and a queued tool call. If the child identity is not tied to the parent’s authority, the supposed kill switch only removes the visible part of the run.

Put the run ID on every piece of work

Cancellation needs an address. Give every run an immutable ID, then propagate it into child runs, gateway decisions, queue messages, provider metadata, and effect receipts.

run_authority:
  run_id: cc-run-4421
  task_id: release-payments-8bc911d
  root_lease: lease-90e4
  child_runs:
    - run_id: cc-run-4421-checks
      lease: lease-90e4-checks
    - run_id: cc-run-4421-ticket
      lease: lease-90e4-ticket
  valid_until: 2026-08-16T11:20:00Z
  revocation_epoch: 17

Every queued message should carry at least run_id, lease, revocation_epoch, operation_id, and an expiry. A worker checks those fields when it starts, not only when the request enters the queue.

That second check closes a common race. A gateway can approve a call at 10:51:03, the operator can revoke the run at 10:51:04, and a worker can pick up the message at 10:51:12. If the worker trusts the old gateway decision without checking current authority, revoked work still runs.

This follows the same rule as checking MCP authority immediately before dispatch. Approval belongs to a specific capability at a specific time. It is not a permanent property of a queue message.

Use a cancellation barrier, not a fire-and-forget signal

A stop request should create a barrier record before the system starts cancelling components:

cancellation_barrier:
  barrier_id: stop-cc-run-4421-01
  run_id: cc-run-4421
  requested_at: 2026-08-16T10:51:04.018Z
  requested_by: operator:thomas
  reason: wrong_commit_detected
  revoke:
    root_lease: lease-90e4
    descendants: true
    new_revocation_epoch: 18
  queue_policy:
    reject_not_started: true
    retain_for_audit: true
  in_flight_policy:
    request_provider_cancel: true
    reconcile_effects: true
  completion_rule:
    pending_calls: 0
    unknown_outcomes: 0
    active_leases: 0

Write the barrier durably first. If the cancellation worker crashes halfway through, another worker can resume from the same record. An in-memory flag disappears at exactly the moment you need it most.

The barrier also gives every service one ordering point. Work stamped with epoch 17 is stale after epoch 18 exists. A late message cannot regain authority because its timestamp looks earlier or because one cache has not refreshed.

Do not delete queued messages silently. Mark them cancelled and preserve their operation IDs. During an incident, you need to know which actions the system rejected and which ones had already crossed into execution.

Treat in-flight calls as unresolved until proven otherwise

Some providers support cancellation. Many only acknowledge that a cancellation request arrived. Others cannot cancel once an operation reaches a commit stage.

Record that difference:

in_flight_call:
  operation_id: reserve-preview-payments-8bc911d
  tool: deployment.reserve_preview
  provider_job: preview-7712
  state_at_barrier: running
  cancellation:
    requested_at: 2026-08-16T10:51:04.233Z
    provider_response: accepted
  effect_check:
    expected_absence:
      reservation_for_run: cc-run-4421
    observed: none
    checked_at: 2026-08-16T10:51:09.804Z
  settled_state: cancelled_without_effect

provider_response: accepted is not the settled state. The provider may race with cancellation and commit the effect first. Query the authoritative system for the expected effect, just as you would after an external write with a missing response.

There are three useful outcomes:

cancelled_without_effect
The provider stopped and authoritative state shows no effect.

completed_and_verified
The operation crossed the commit point. Record the effect and start the approved recovery path.

unknown
The system cannot prove whether an effect occurred. Keep the barrier open and assign reconciliation to a person.

Do not translate unknown into cancelled. That makes the dashboard look tidy while leaving the operator blind to a possible production change.

Make descendants lose authority with the parent

A child run should receive less authority than its parent and should never outlive the parent’s lease unless a human explicitly promotes it to an independent task.

The gateway can enforce the relationship:

child_dispatch_check:
  child_run: cc-run-4421-ticket
  parent_run: cc-run-4421
  child_epoch: 17
  current_parent_epoch: 18
  parent_lease_status: revoked
  decision: deny_parent_revoked
  external_call_sent: false

Do not rely on the parent to notify every child. The parent may be the process that crashed. Workers and gateways should resolve current authority from the control plane before starting consequential work.

Also check scheduled retries. A retry timer is queued authority in another form. When the barrier opens, cancel timers, callbacks, delayed messages, and workflow continuations attached to the run. Otherwise the system can look quiet for five minutes and wake up under stale authority.

Return a stop receipt that can be reviewed

The operator needs evidence, not a green icon. Produce a stop receipt only after the completion rule passes:

stop_receipt:
  barrier_id: stop-cc-run-4421-01
  run_id: cc-run-4421
  authority:
    root_lease: revoked
    descendant_leases_revoked: 2
    current_epoch: 18
  work:
    queued_cancelled: 3
    in_flight_settled: 1
    scheduled_retries_cancelled: 2
  effects:
    completed_and_verified: 0
    cancelled_without_effect: 1
    unknown: 0
  last_settled_at: 2026-08-16T10:51:09.804Z
  quiet_period_seconds: 30
  verified_at: 2026-08-16T10:51:39.921Z
  decision: stopped_and_quiet

The quiet period catches delayed callbacks and queue visibility lag. Its duration should match the systems involved rather than use one arbitrary global value. A local worker queue and a third-party deployment service have different settling behaviour.

Put this receipt in the review packet. If an effect completed before cancellation, include its effect receipt and rollback owner. If anything remains unknown, the final decision must say stop_incomplete and name the person holding the reconciliation task.

Test the stop path while nothing is on fire

A stop control that works only when every dependency is healthy is decoration. Add fixtures for the races you expect to see:

cancellation_evals:
  - case: queued_call_starts_after_parent_revocation
    expect: deny_parent_revoked
  - case: provider_commits_during_cancel_request
    expect: completed_and_verified
  - case: cancellation_worker_crashes_mid_barrier
    expect: resume_from_durable_barrier
  - case: delayed_retry_fires_after_revocation
    expect: deny_stale_epoch
  - case: provider_status_unavailable
    expect: stop_incomplete_unknown_outcome
  - case: child_run_misses_parent_notification
    expect: deny_on_gateway_authority_check

Run the test with a provider stub that deliberately commits during the cancellation race. Then break the status endpoint. The system should preserve the unknown outcome rather than invent a clean cancellation.

My rule is stricter than “the process exited.” A Claude Code run is stopped only when its authority is revoked, its pending work is drained, and every in-flight effect is settled or handed to a named operator.

Claude Code: Building Production Agents That Actually Work covers MCP boundaries, permission leases, effect verification, rollback, evals, observability, and review packets for production coding-agent work.