How-To

Agent Session Handoffs: A Portable Artifact Guide

Preserve interrupted AI agent work with a vendor-neutral, schema-validated handoff that records diffs, tests, failures, approvals, and safe recovery.

  • #AI agents
  • #coding agents
  • #session handoff
  • #structured output
  • #CI artifacts

An agent session can stop while the work is still valuable. A usage limit may arrive mid-task, a long prompt may fail, a permission may require a human, a CI job may end, or the operator may move from one app to another. The fragile response is to ask the next agent to infer the state from a conversational summary. The reliable response is to leave a small, validated handoff artifact beside the work.

This guide defines that artifact. It is vendor-neutral: OpenAI Codex, Claude Code, another coding agent, a CI worker, or a human can produce it. It does not pretend that products share session formats or that one provider can resume another provider’s private transcript. It preserves the facts needed to inspect and continue the task safely: objective, scope, assumptions, changed files, raw diff location, exact commands and full outcomes, failures, approvals still required, redactions, the next safe command, stop reason, and completion criteria.

All product behavior and retention facts in this article were checked against official sources on 2026-07-24. Those facts are dated because command flags, plan behavior, and artifact-retention rules can change.

Why interrupted work needs a file, not better memory

Demand is visible in public reports. One OpenAI Codex issue asks for automatic recovery after a usage limit interrupts work mid-task (openai/codex issue 21073). Another reports unexpectedly rapid limit consumption in a CLI session (openai/codex issue 2448). Claude Code users have reported “prompt is too long” failures during active editing (anthropics/claude-code issue 8878) and hangs in long sessions with many tool calls and large outputs (anthropics/claude-code issue 26224). Users also compare the practical amount of work delivered by monthly Codex and Claude plans, not only model quality (Codex versus Claude usage-limit discussion), while multi-app subscribers compare which recurring tools earn a place in a monthly stack (four AI subscriptions discussion).

These links are demand evidence, not authoritative product specifications. Reports can reflect temporary bugs, individual workflows, account-specific limits, or later product changes. The durable conclusion is narrower: people lose momentum when work is interrupted, and they evaluate agent subscriptions partly by recoverable work per month. A portable checkpoint reduces the cost of interruption without assuming any particular plan, quota, or model.

A provider session is useful but insufficient as the only recovery mechanism. It may be tied to one account, tool, machine, directory, retention policy, or transcript format. A transcript is also noisy: it records exploration, rejected ideas, and tool output, but may not clearly identify the final changed files, the exact failed check, or the permission that remains outstanding.

The handoff artifact is not a replacement for version control, a raw patch, CI logs, or the provider’s session. It is the index that points to those records and states what they prove.

Narrative summary versus machine-checkable handoff

Both forms have value. A concise narrative helps a human understand intent. A structured artifact prevents omissions and lets software reject an unsafe or incomplete handoff.

QuestionNarrative summaryMachine-checkable handoff artifact
Can a human skim it quickly?Usually excellentGood when accompanied by a short summary
Can CI require every safety field?No reliable guaranteeYes, with JSON Schema validation
Can it distinguish “not run” from “failed”?Often ambiguousYes, through explicit command status
Can it link the exact raw diff and logs?Possible but easy to omitRequired fields can make links mandatory
Can tools compare two handoffs?Difficult and error-proneStable keys support deterministic comparison
Does it prove the code is correct?NoNo; it records evidence, not truth
Does it safely resume a provider session?Not by itselfNo; resumption remains provider-specific and separately authorized

The best pattern is dual-layered:

  1. summary is a short, factual narrative for people.
  2. The remaining fields form a strict record for validators and downstream tools.
  3. Large or sensitive evidence stays in separate files; the artifact stores a location, digest, retention note, and redaction status.

If your workflow already uses machine gates for AI output, validate the handoff at the same boundary. If continuation could trigger a consequential action, apply the pause-and-resume rules in the human approval gate guide.

Provider capabilities and the portable convention are different layers

Current tools already expose useful primitives. Use them, but do not confuse them with the proposed convention.

Layer or productOfficial capability verified 2026-07-24What it does not establish
OpenAI Codex CLIcodex exec resume can continue a non-interactive session; --last scopes selection to the current working directory unless broadened; --json emits JSONL events; --output-schema requests a schema-conforming final response (Codex command reference, non-interactive mode)It does not create a vendor-neutral evidence contract or prove external files and approvals remain unchanged
Claude Code--continue and --resume reopen sessions; local transcripts are JSONL; permission rules can allow, ask, or deny tools (Claude Code sessions, permissions)Its transcript format and permission state are not a portable handoff contract
Gitgit diff compares working trees, indexes, commits, or blobs; patch and raw formats have documented semantics (git-diff, diff format)A diff does not record the objective, failed commands, missing approvals, or the safe next action
GitHub ActionsWorkflow artifacts can retain logs, test results, failures, screenshots, and other outputs after a job ends (workflow artifacts)Artifact storage does not define the handoff fields or guarantee permanent retention
Portable convention in this guideA small JSON document with required evidence and safety fields, validated against a published schemaIt cannot import a provider’s hidden state, authorize actions, or replace review

This distinction prevents two common errors. First, “the session can resume” does not mean it should resume: the repository, permission set, branch, secrets, or requirements may have changed. Second, “the artifact validates” does not mean the task is correct: schema validation proves shape and required declarations, not the truth of those declarations.

For broader operating controls around coding agents, start with the AI coding agents team guide and keep repository-specific rules in the pattern described by the AGENTS.md writing guide.

The handoff contract

Call the document agent-handoff.json unless the repository defines another name. Keep it in an approved task-artifact location, not automatically in the source tree. A portable handoff has four design rules:

  • Self-identifying: it declares the convention and schema version.
  • Evidence-linked: it points to raw patches and logs instead of paraphrasing them away.
  • Fail-closed: unknown state, missing evidence, and ungranted approval remain explicit blockers.
  • Privacy-minimized: it contains no credentials, personal data, or unnecessary prompt content.

Required fields

FieldRequired contentWhy the next operator needs it
artifact_versionConvention version, such as 1.0.0Selects the correct validator and migration rules
handoff_idLocally unique, non-secret identifierCorrelates the artifact, logs, and review
created_atRFC 3339 timestamp with offsetEstablishes freshness without guessing
producerTool name and optional session referenceIdentifies origin without embedding a transcript
objectiveDesired outcome in one or two sentencesPrevents continuation toward the wrong goal
scopeIncluded and excluded paths or actionsBlocks quiet scope expansion
assumptionsExplicit statements with verification statusSeparates evidence from inference
changed_filesPath, status, purpose, and verification stateMakes the review surface enumerable
raw_diffLocation, format, digest, and capture statusPreserves reviewable file-level truth
commandsExact command, directory, exit code, and full-output locationDistinguishes executed evidence from claimed evidence
failuresError class, observation, impact, and attempted responseStops the next agent from repeating a failed path blindly
pending_approvalsExact action, reason, approver role, and stateKeeps “not authorized” distinct from “not attempted”
redactionPolicy, removed categories, and verificationPrevents a clean-looking artifact from leaking secrets
next_safe_commandOne read-only or bounded command, or nullGives a conservative first action, not an execution mandate
stop_reasonEnumerated reason and factual detailExplains why work ended
completion_criteriaCriterion plus met, unmet, or unknownPrevents partial progress from being labeled complete
statuscomplete, interrupted, blocked, or failedSupports routing without parsing prose

Use repository-relative paths when the evidence remains inside the same workspace. Use an HTTPS URL only when access is controlled and expected to remain available. Avoid machine-specific absolute paths in a cross-machine artifact; if an absolute path is unavoidable, identify it as host-local.

Evidence should remain raw, addressable, and finite

A handoff must preserve full outcomes without inflating the JSON into a transcript dump. Store short output inline only when it is plainly non-sensitive and bounded. Store long output in a separate artifact and provide:

  • the location;
  • content type and encoding;
  • a SHA-256 digest;
  • byte length;
  • whether the output is complete or truncated;
  • the reason and byte limit if truncated;
  • retention or expiry information;
  • a redaction declaration.
EvidenceRecommended representationMinimum integrity metadataUnsafe shortcut
Text patch.patch or provider-neutral unified diffSHA-256, bytes, capture command or method“Changed authentication logic”
Changed-file listArray in the JSON artifactPath and status for every item“Several files changed”
Test outputPlain text plus JUnit XML when availableCommand, exit code, output location, digest“Tests look good”
Build or lint outputPlain text logCommand, working directory, exit codeCopying only the last success line
Research evidenceSource ledger with URL, access date, and claim mappingComplete URL and checked dateA prose claim with no source
Visual evidenceScreenshot or report artifactFile type, digest, capture context“The page looked correct”

Git’s official documentation gives two useful representations. --numstat is designed for easier machine consumption, while -p produces patch text for review (git-diff documentation). The handoff should normally list files structurally and point to a complete patch. It should not paste a massive patch into the main JSON.

Do not run a Git command merely to fill this artifact when the current operating boundary forbids Git. A producer may use an approved non-Git diff tool, a workspace snapshot service, or mark raw_diff.capture_status as unavailable with an explanation. Never invent a patch digest or claim a capture ran.

The AI-generated code review guide explains why reviewers must inspect the actual diff rather than trust the author’s summary. For independent workspaces, the Git worktrees guide shows how to reduce cross-task contamination, but a worktree does not remove the need for a handoff.

A complete JSON handoff example

The following document is valid JSON. Its domain names and identifiers are deliberately non-operational.

{
  "artifact_version": "1.0.0",
  "handoff_id": "handoff-20260724-0017",
  "created_at": "2026-07-24T14:32:19+09:00",
  "producer": {
    "tool": "example-coding-agent",
    "session_reference": "local-session-redacted",
    "provider_resume_required": false
  },
  "objective": "Fix duplicate invoice retry scheduling without changing public API behavior.",
  "summary": "A bounded change and regression test were written. The focused test passed, but the full suite was interrupted before completion.",
  "scope": {
    "included": [
      "src/retry/scheduler.ts",
      "tests/retry/scheduler.test.ts"
    ],
    "excluded": [
      "dependency upgrades",
      "database migrations",
      "deployment"
    ]
  },
  "assumptions": [
    {
      "statement": "Retry keys are unique per tenant and invoice.",
      "state": "verified",
      "evidence": "artifacts/inspection/retry-key-callers.txt"
    },
    {
      "statement": "No external consumer depends on duplicate scheduling.",
      "state": "unknown",
      "evidence": null
    }
  ],
  "changed_files": [
    {
      "path": "src/retry/scheduler.ts",
      "status": "modified",
      "purpose": "Reject a duplicate key before enqueueing.",
      "verification": "focused-test-passed"
    },
    {
      "path": "tests/retry/scheduler.test.ts",
      "status": "modified",
      "purpose": "Reproduce duplicate enqueue and assert one scheduled job.",
      "verification": "focused-test-passed"
    }
  ],
  "raw_diff": {
    "location": "artifacts/diffs/handoff-20260724-0017.patch",
    "format": "unified-diff",
    "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    "bytes": 0,
    "capture_status": "placeholder-example-only"
  },
  "commands": [
    {
      "command": "node --test tests/retry/scheduler.test.ts",
      "working_directory": ".",
      "started_at": "2026-07-24T14:25:02+09:00",
      "finished_at": "2026-07-24T14:25:04+09:00",
      "exit_code": 0,
      "outcome": "passed",
      "full_output": {
        "location": "artifacts/logs/focused-test.txt",
        "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
        "bytes": 0,
        "complete": true,
        "redacted": true
      }
    },
    {
      "command": "node --test",
      "working_directory": ".",
      "started_at": "2026-07-24T14:27:11+09:00",
      "finished_at": "2026-07-24T14:32:17+09:00",
      "exit_code": null,
      "outcome": "interrupted",
      "full_output": {
        "location": "artifacts/logs/full-test-interrupted.txt",
        "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
        "bytes": 0,
        "complete": false,
        "redacted": true
      }
    }
  ],
  "failures": [
    {
      "class": "usage-limit",
      "observed": "The agent stopped while the full test command was still running.",
      "impact": "Full-suite result is unknown.",
      "attempted_response": "Saved the partial log and made no further tool calls."
    }
  ],
  "pending_approvals": [
    {
      "action": "Run tests that require the private integration environment.",
      "reason": "The environment is outside the current permission scope.",
      "approver_role": "repository maintainer",
      "state": "pending"
    }
  ],
  "redaction": {
    "policy": "handoff-minimize-v1",
    "categories_removed": [
      "access tokens",
      "customer identifiers",
      "absolute user paths"
    ],
    "verification": "manual-and-pattern-scan",
    "contains_personal_data": false,
    "contains_secrets": false
  },
  "next_safe_command": {
    "command": "node --test tests/retry/scheduler.test.ts",
    "working_directory": ".",
    "why_safe": "Focused local verification with no network or external side effect."
  },
  "stop_reason": {
    "code": "usage-limit",
    "detail": "Session capacity ended before the full suite returned."
  },
  "completion_criteria": [
    {
      "criterion": "Duplicate scheduling is reproduced by a regression test.",
      "state": "met",
      "evidence": "artifacts/logs/focused-test.txt"
    },
    {
      "criterion": "Focused regression test passes.",
      "state": "met",
      "evidence": "artifacts/logs/focused-test.txt"
    },
    {
      "criterion": "Full test suite passes.",
      "state": "unknown",
      "evidence": "artifacts/logs/full-test-interrupted.txt"
    },
    {
      "criterion": "A human reviews the raw diff.",
      "state": "unmet",
      "evidence": null
    }
  ],
  "status": "interrupted"
}

The example uses the well-known SHA-256 value for an empty byte string only to keep the sample internally parseable and obviously non-production. A real artifact must calculate digests from real evidence. The placeholder-example-only capture status prevents this sample from masquerading as valid proof.

JSON Schema for the portable artifact

The schema uses JSON Schema Draft 2020-12. The official JSON Schema guide recommends declaring the dialect with $schema; required makes named properties mandatory, and additionalProperties: false rejects undeclared keys (JSON Schema basics, object reference). Format values such as date-time may be annotations depending on the validator, so CI should use a validator configured to assert the formats it relies on.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://ctxwire.example/schemas/agent-handoff-1.0.0.schema.json",
  "title": "Portable Agent Session Handoff",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "artifact_version",
    "handoff_id",
    "created_at",
    "producer",
    "objective",
    "summary",
    "scope",
    "assumptions",
    "changed_files",
    "raw_diff",
    "commands",
    "failures",
    "pending_approvals",
    "redaction",
    "next_safe_command",
    "stop_reason",
    "completion_criteria",
    "status"
  ],
  "properties": {
    "artifact_version": {
      "const": "1.0.0"
    },
    "handoff_id": {
      "type": "string",
      "minLength": 8,
      "maxLength": 128,
      "pattern": "^[A-Za-z0-9._-]+$"
    },
    "created_at": {
      "type": "string",
      "format": "date-time"
    },
    "producer": {
      "type": "object",
      "additionalProperties": false,
      "required": ["tool", "session_reference", "provider_resume_required"],
      "properties": {
        "tool": {"type": "string", "minLength": 1},
        "session_reference": {"type": ["string", "null"]},
        "provider_resume_required": {"type": "boolean"}
      }
    },
    "objective": {
      "type": "string",
      "minLength": 10,
      "maxLength": 1000
    },
    "summary": {
      "type": "string",
      "minLength": 10,
      "maxLength": 2000
    },
    "scope": {
      "type": "object",
      "additionalProperties": false,
      "required": ["included", "excluded"],
      "properties": {
        "included": {
          "type": "array",
          "items": {"type": "string"},
          "minItems": 1,
          "uniqueItems": true
        },
        "excluded": {
          "type": "array",
          "items": {"type": "string"},
          "uniqueItems": true
        }
      }
    },
    "assumptions": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["statement", "state", "evidence"],
        "properties": {
          "statement": {"type": "string", "minLength": 1},
          "state": {"enum": ["verified", "unverified", "unknown", "rejected"]},
          "evidence": {"type": ["string", "null"]}
        }
      }
    },
    "changed_files": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["path", "status", "purpose", "verification"],
        "properties": {
          "path": {"type": "string", "minLength": 1},
          "status": {"enum": ["added", "modified", "deleted", "renamed", "untracked"]},
          "purpose": {"type": "string", "minLength": 1},
          "verification": {"type": "string", "minLength": 1}
        }
      }
    },
    "raw_diff": {
      "type": "object",
      "additionalProperties": false,
      "required": ["location", "format", "sha256", "bytes", "capture_status"],
      "properties": {
        "location": {"type": ["string", "null"]},
        "format": {"enum": ["unified-diff", "git-raw", "provider-diff", "none"]},
        "sha256": {
          "type": ["string", "null"],
          "pattern": "^[a-f0-9]{64}$"
        },
        "bytes": {"type": ["integer", "null"], "minimum": 0},
        "capture_status": {
          "enum": ["captured", "unavailable", "not-applicable", "placeholder-example-only"]
        }
      }
    },
    "commands": {
      "type": "array",
      "items": {"$ref": "#/$defs/command"}
    },
    "failures": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["class", "observed", "impact", "attempted_response"],
        "properties": {
          "class": {"type": "string", "minLength": 1},
          "observed": {"type": "string", "minLength": 1},
          "impact": {"type": "string", "minLength": 1},
          "attempted_response": {"type": "string", "minLength": 1}
        }
      }
    },
    "pending_approvals": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["action", "reason", "approver_role", "state"],
        "properties": {
          "action": {"type": "string", "minLength": 1},
          "reason": {"type": "string", "minLength": 1},
          "approver_role": {"type": "string", "minLength": 1},
          "state": {"enum": ["pending", "approved", "rejected", "expired"]}
        }
      }
    },
    "redaction": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "policy",
        "categories_removed",
        "verification",
        "contains_personal_data",
        "contains_secrets"
      ],
      "properties": {
        "policy": {"type": "string", "minLength": 1},
        "categories_removed": {
          "type": "array",
          "items": {"type": "string"},
          "uniqueItems": true
        },
        "verification": {"type": "string", "minLength": 1},
        "contains_personal_data": {"const": false},
        "contains_secrets": {"const": false}
      }
    },
    "next_safe_command": {
      "oneOf": [
        {"type": "null"},
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["command", "working_directory", "why_safe"],
          "properties": {
            "command": {"type": "string", "minLength": 1},
            "working_directory": {"type": "string", "minLength": 1},
            "why_safe": {"type": "string", "minLength": 1}
          }
        }
      ]
    },
    "stop_reason": {
      "type": "object",
      "additionalProperties": false,
      "required": ["code", "detail"],
      "properties": {
        "code": {
          "enum": [
            "complete",
            "usage-limit",
            "context-limit",
            "command-timeout",
            "tool-failure",
            "permission-required",
            "approval-required",
            "environment-failure",
            "operator-stop",
            "unknown"
          ]
        },
        "detail": {"type": "string", "minLength": 1}
      }
    },
    "completion_criteria": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["criterion", "state", "evidence"],
        "properties": {
          "criterion": {"type": "string", "minLength": 1},
          "state": {"enum": ["met", "unmet", "unknown"]},
          "evidence": {"type": ["string", "null"]}
        }
      }
    },
    "status": {
      "enum": ["complete", "interrupted", "blocked", "failed"]
    }
  },
  "$defs": {
    "evidence_file": {
      "type": "object",
      "additionalProperties": false,
      "required": ["location", "sha256", "bytes", "complete", "redacted"],
      "properties": {
        "location": {"type": "string", "minLength": 1},
        "sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
        "bytes": {"type": "integer", "minimum": 0},
        "complete": {"type": "boolean"},
        "redacted": {"const": true}
      }
    },
    "command": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "command",
        "working_directory",
        "started_at",
        "finished_at",
        "exit_code",
        "outcome",
        "full_output"
      ],
      "properties": {
        "command": {"type": "string", "minLength": 1},
        "working_directory": {"type": "string", "minLength": 1},
        "started_at": {"type": "string", "format": "date-time"},
        "finished_at": {"type": "string", "format": "date-time"},
        "exit_code": {"type": ["integer", "null"]},
        "outcome": {
          "enum": ["passed", "failed", "interrupted", "not-run"]
        },
        "full_output": {"$ref": "#/$defs/evidence_file"}
      }
    }
  }
}

The schema intentionally requires negative declarations for secrets and personal data. That is stricter than accepting an omitted field, but it is not magical detection. The producing workflow must still scan and review the artifact. For implementation patterns, use the PII redaction guide and the LLM data retention checklist.

Four interrupted-task examples

The following examples are complete human-readable renderings of the same contract. In production, serialize them as JSON and validate them with the schema. Each example states what happened, what evidence exists, what remains unknown, and what the next operator may safely do.

Example 1: bug fix interrupted during the full test suite

artifact_version: 1.0.0
handoff_id: bugfix-duplicate-retry-20260724
created_at: 2026-07-24T14:32:19+09:00
producer: example-coding-agent; provider session reference withheld
status: interrupted

objective:
  Prevent duplicate invoice retry jobs without changing public API behavior.

scope:
  included:
    - src/retry/scheduler.ts
    - tests/retry/scheduler.test.ts
  excluded:
    - dependencies
    - schema migrations
    - deployment

assumptions:
  - VERIFIED: retry keys combine tenant ID and invoice ID.
    Evidence: artifacts/inspection/retry-key-callers.txt
  - UNKNOWN: no external consumer depends on duplicate scheduling.

changed_files:
  - MODIFIED src/retry/scheduler.ts
    Purpose: reject a duplicate key before enqueue.
    Verification: focused test passed.
  - MODIFIED tests/retry/scheduler.test.ts
    Purpose: reproduce the bug and assert one queued job.
    Verification: focused test passed.

raw_diff:
  location: artifacts/diffs/bugfix-duplicate-retry.patch
  format: unified-diff
  digest: recorded in the JSON artifact

commands_and_full_outcomes:
  - node --test tests/retry/scheduler.test.ts
    cwd: repository root
    exit_code: 0
    outcome: passed
    full_output: artifacts/logs/focused-test.txt
  - node --test
    cwd: repository root
    exit_code: unknown because the process was interrupted
    outcome: interrupted
    full_output: artifacts/logs/full-test-interrupted.txt

failures:
  - usage limit stopped the session while the full suite was active.
  - impact: the full-suite result is unknown.
  - response: partial output was preserved; no retry was attempted.

pending_approvals:
  - private integration environment access is pending from a repository maintainer.

redaction:
  - removed access tokens, customer identifiers, and absolute user paths.
  - contains_secrets: false
  - contains_personal_data: false

next_safe_command:
  node --test tests/retry/scheduler.test.ts
  Reason: bounded local verification with no network or external side effect.

stop_reason:
  usage-limit

completion_criteria:
  - MET: regression test reproduces duplicate scheduling.
  - MET: focused test passes.
  - UNKNOWN: full suite passes.
  - UNMET: human review of the raw diff.

This handoff does not say “nearly done.” It exposes two missing facts. The next operator can re-run the focused test after inspecting the workspace, but must not convert the unknown full-suite result into a pass.

Example 2: research interrupted after source collection

artifact_version: 1.0.0
handoff_id: research-artifact-retention-20260724
created_at: 2026-07-24T16:08:00+09:00
producer: example-research-agent; session reference unavailable
status: interrupted

objective:
  Compare official CI artifact-retention controls for a vendor-neutral
  evidence-storage recommendation.

scope:
  included:
    - GitHub Actions official documentation
    - GitLab CI official documentation
    - CircleCI official documentation
    - Azure Pipelines official documentation
  excluded:
    - vendor ranking
    - price recommendation
    - account configuration changes

assumptions:
  - VERIFIED: retention is a policy property, not a permanence guarantee.
    Evidence: official provider pages in sources/retention-ledger.json
  - UNVERIFIED: all enterprise overrides are visible to ordinary project members.

changed_files:
  - ADDED sources/retention-ledger.json
    Purpose: map each factual claim to an official URL and checked date.
    Verification: parses as JSON.
  - ADDED notes/retention-comparison.md
    Purpose: draft comparison language.
    Verification: not editorially reviewed.

raw_diff:
  location: artifacts/diffs/research-artifact-retention.patch
  format: unified-diff
  digest: recorded in the JSON artifact

commands_and_full_outcomes:
  - validate-json sources/retention-ledger.json
    cwd: repository root
    exit_code: 0
    outcome: passed
    full_output: artifacts/logs/source-ledger-validation.txt
  - link-check sources/retention-ledger.json
    cwd: repository root
    exit_code: 1
    outcome: failed
    full_output: artifacts/logs/link-check.txt

failures:
  - one Azure documentation locale redirected unexpectedly.
  - impact: the Azure citation needs a canonical English URL check.
  - response: preserved the observed URL and did not rewrite the claim.

pending_approvals:
  - none.

redaction:
  - no account pages, private repository names, or signed artifact URLs copied.
  - contains_secrets: false
  - contains_personal_data: false

next_safe_command:
  Re-run the link checker against the source ledger without authentication.
  Reason: read-only validation of public URLs.

stop_reason:
  tool-failure

completion_criteria:
  - MET: four official provider sources collected.
  - MET: claims carry checked dates.
  - UNMET: every canonical URL returns successfully.
  - UNMET: final comparison receives editorial review.

Research handoffs need claim-level evidence, not only a reading list. Record the exact claim supported by each source, the checked date, and any conflict. The automation case-study evidence checklist provides a deeper method for separating measured results, documents, estimates, and unsupported claims.

Example 3: migration stopped before any production change

artifact_version: 1.0.0
handoff_id: migration-customer-state-20260724
created_at: 2026-07-24T18:41:05+09:00
producer: example-database-agent; local session ID redacted
status: blocked

objective:
  Prepare a reversible migration that replaces nullable customer state text
  with a constrained status field.

scope:
  included:
    - migration source file
    - rollback source file
    - local fixture database
    - dry-run logs
  excluded:
    - staging execution
    - production execution
    - credential creation
    - data deletion

assumptions:
  - VERIFIED: all current values map to active, paused, or closed in fixtures.
    Evidence: artifacts/logs/fixture-value-audit.txt
  - UNKNOWN: production contains no unmapped historical values.

changed_files:
  - ADDED migrations/20260724_customer_status_up.sql
    Purpose: add, backfill, validate, and constrain the new status column.
    Verification: local fixture dry run passed.
  - ADDED migrations/20260724_customer_status_down.sql
    Purpose: remove the new constraint and column in a local rollback.
    Verification: local fixture rollback passed.
  - MODIFIED tests/migrations/customer-status.test.sql
    Purpose: assert mappings and reject an unmapped value.
    Verification: local fixture tests passed.

raw_diff:
  location: artifacts/diffs/migration-customer-state.patch
  format: unified-diff
  digest: recorded in the JSON artifact

commands_and_full_outcomes:
  - db-fixture reset
    cwd: repository root
    exit_code: 0
    outcome: passed
    full_output: artifacts/logs/fixture-reset.txt
  - db-migrate --environment local-fixture --dry-run
    cwd: repository root
    exit_code: 0
    outcome: passed
    full_output: artifacts/logs/migration-dry-run.txt
  - db-migrate --environment local-fixture
    cwd: repository root
    exit_code: 0
    outcome: passed
    full_output: artifacts/logs/migration-forward.txt
  - db-rollback --environment local-fixture
    cwd: repository root
    exit_code: 0
    outcome: passed
    full_output: artifacts/logs/migration-rollback.txt
  - db-migrate --environment production
    cwd: repository root
    exit_code: null
    outcome: not-run
    full_output: artifacts/logs/production-not-run-declaration.txt

failures:
  - none observed in the local fixture.
  - remaining risk: fixture values may not represent production history.

pending_approvals:
  - production value audit requires a data-owner-approved read path.
  - staging migration requires the designated change approver.
  - production migration is a separate approval after staging evidence.

redaction:
  - fixture data is synthetic.
  - database URLs, user names, and credentials are absent.
  - contains_secrets: false
  - contains_personal_data: false

next_safe_command:
  Parse both migration files with the repository's local SQL linter.
  Reason: local, read-only analysis that cannot touch a database.

stop_reason:
  approval-required

completion_criteria:
  - MET: forward migration passes on a local fixture.
  - MET: rollback passes on a local fixture.
  - MET: unmapped fixture values fail closed.
  - UNKNOWN: production values are fully mapped.
  - UNMET: staging approval and execution.
  - UNMET: production approval and execution.

The important line is not-run. A migration command listed with a null exit code is not evidence of a failed production run; it is an explicit declaration that no production action occurred. The next agent must not infer authority from the existence of prepared SQL.

Example 4: content production interrupted before publication

artifact_version: 1.0.0
handoff_id: content-security-guide-20260724
created_at: 2026-07-24T20:05:44+09:00
producer: example-content-agent; provider reference withheld
status: interrupted

objective:
  Draft an evidence-led security guide with official sources and valid internal links.

scope:
  included:
    - one Markdown draft
    - official-source ledger
    - local editorial checks
  excluded:
    - publishing
    - deployment
    - email or social distribution
    - account connection

assumptions:
  - VERIFIED: frontmatter fields match the local content schema.
    Evidence: artifacts/logs/frontmatter-check.txt
  - UNKNOWN: every volatile product statement remains current after 2026-07-24.

changed_files:
  - ADDED src/content/articles/example-security-guide.md
    Purpose: reader-facing draft.
    Verification: frontmatter and internal-link checks passed; human edit pending.
  - ADDED sources/example-security-guide.json
    Purpose: claim-to-source ledger.
    Verification: JSON parse passed.

raw_diff:
  location: artifacts/diffs/content-security-guide.patch
  format: unified-diff
  digest: recorded in the JSON artifact

commands_and_full_outcomes:
  - content-schema-check src/content/articles/example-security-guide.md
    cwd: repository root
    exit_code: 0
    outcome: passed
    full_output: artifacts/logs/frontmatter-check.txt
  - internal-link-check src/content/articles/example-security-guide.md
    cwd: repository root
    exit_code: 0
    outcome: passed
    full_output: artifacts/logs/internal-links.txt
  - editorial-claim-check src/content/articles/example-security-guide.md
    cwd: repository root
    exit_code: 2
    outcome: failed
    full_output: artifacts/logs/claim-check.txt

failures:
  - two volatile product statements lacked checked dates.
  - impact: the draft is not ready for publication.
  - response: marked the statements in the claim ledger; did not publish.

pending_approvals:
  - editorial approval is pending.
  - publication and deployment each require separate authorization.

redaction:
  - removed private interview notes, email addresses, analytics IDs, and draft tokens.
  - contains_secrets: false
  - contains_personal_data: false

next_safe_command:
  Run the local claim checker after dates are added.
  Reason: local validation with no external side effect.

stop_reason:
  tool-failure

completion_criteria:
  - MET: content schema passes.
  - MET: internal links resolve locally.
  - UNMET: every volatile claim has an official source and date.
  - UNMET: editorial approval.
  - UNMET: publication approval.

Content has its own irreversible boundary. “Draft complete” must never silently become “published.” Keep creation, editorial approval, and publication as distinct states.

Stop reasons and safe continuation

Use a small, controlled vocabulary for stop_reason.code. Free text belongs in detail.

Stop codeMeaningSafe default
completeEvery declared criterion is met and evidence is presentReview and archive; do not repeat work automatically
usage-limitProvider or plan capacity stopped progressPreserve state; continue only after capacity and environment checks
context-limitThe session could not reliably carry more contextStart a fresh session from the artifact after inspecting evidence
command-timeoutA command exceeded its bounded runtimeInspect partial logs and process state before any retry
tool-failureA required tool errored or returned invalid outputDiagnose the tool; do not assume the task result
permission-requiredRuntime permission is missingAsk for the narrow required permission; do not bypass
approval-requiredA consequential action awaits explicit authorizationFreeze the exact action and wait
environment-failureWorkspace, dependency, CI, or host became unreliableRe-establish a known state before continuation
operator-stopA human deliberately ended the runPreserve the stated reason and do not auto-resume
unknownThe producer cannot determine the causeFail closed and begin with read-only inspection

next_safe_command is a recommendation, not authority. Before running it, the receiver must confirm:

  1. the handoff is fresh enough for the task;
  2. the workspace and branch match the recorded scope;
  3. the command is still allowed by current repository rules;
  4. its working directory exists;
  5. it has no hidden network, credential, migration, deployment, publication, or deletion effect;
  6. pending approvals remain unresolved unless a verifiable approval record says otherwise.

If no command is predictably safe, use null. A precise null is better than a speculative shell command.

Provider resume features can help after this inspection. As of 2026-07-24, OpenAI documents interactive and non-interactive resume commands, including session-ID and current-directory selection behavior (Codex command reference). Anthropic documents claude --continue, claude --resume, named sessions, and session IDs (Claude Code session management). These capabilities are conveniences, not authorization. The artifact is still valuable when the original session is unavailable or when a clean context is safer.

Permissions and approvals belong in the artifact, not in optimistic prose

Permissions describe what the runtime can do. Approvals describe what a responsible person has authorized for this particular action. Keep them separate.

As of 2026-07-24, Codex documents sandbox modes for model-generated commands and machine-readable non-interactive output (Codex non-interactive mode). Claude Code documents ordered deny, ask, and allow permission rules, with deny taking precedence (Claude Code permissions). Those are product controls. The portable artifact records the effective boundary observed by the producer and any exact action still awaiting approval.

A pending approval entry should contain:

  • the proposed action in concrete terms;
  • target environment or destination;
  • why ordinary task authority is insufficient;
  • required approver role, not a guessed person’s identity;
  • request identifier if an approval system supplied one;
  • expiry or freshness rule when known;
  • current state.

Never record “user approved” merely because a model said so, a timeout elapsed, or a prior similar action was approved. Do not copy approval links containing bearer tokens. Store a non-secret request ID and the approved-action digest instead.

For a fuller design, see human-in-the-loop AI agents and the coding-agent sandbox guide.

What must never be copied

The handoff exists to reduce lost work, not to maximize captured context. Data minimization is a feature.

Never copy into the handoffWhy it is unsafeSafer representation
API keys, OAuth tokens, session cookies, passwords, recovery codes, private keysDirect credential compromiseSecret reference name plus “available/not available”; never the value
Signed download URLs or presigned object-store linksThe URL may itself grant accessStable artifact ID and access-controlled retrieval procedure
Customer names, email addresses, phone numbers, addresses, account IDsUnnecessary personal-data exposureTyped placeholders or aggregate counts
Health, financial, biometric, employment, or precise-location dataHigh-impact sensitive dataOmit; if essential, use an approved protected evidence system
Full private prompts or transcriptsThey may contain secrets, personal data, private source, and untrusted instructionsMinimal objective, assumptions, and evidence pointers
Private source code pasted into an external handoff serviceExpands the disclosure boundaryRepository-relative path and local digest
Absolute home-directory paths and user namesLeaks machine and identity details; reduces portabilityRepository-relative paths or <workspace>
Raw database rows, production dumps, and real migration samplesData exposure and retention riskSynthetic fixtures, counts, and schema-level observations
Unredacted logs and environment dumpsOften contain credentials and identifiersFiltered logs with redaction policy and scan result
Personal opinions about named employees or customersIrrelevant and potentially harmfulRole-based factual blocker without identity

Redaction must happen before the artifact leaves the controlled workspace. Replacing a token after it has been uploaded does not undo the original disclosure. Scan both the main JSON and every linked evidence file. Also scan filenames: an otherwise clean path can contain a customer name or incident identifier.

Use irreversible placeholders when the original value is not needed. If correlation is necessary, generate a task-scoped opaque label such as PERSON-1; do not use a stable global hash that can link activity across tasks. Do not include the reversible mapping in the handoff.

CI artifacts, logs, and retention

A CI provider can store the JSON, patch, test reports, and logs, but retention varies and is policy-dependent. Record the expected expiry in the handoff or in a manifest beside it.

ProviderOfficially documented behavior checked 2026-07-24Handoff implication
GitHub ActionsWorkflow artifacts persist outputs after a job. GitHub documents a 90-day default for artifacts and logs, configurable within repository-type limits; retention-days can be set for an individual uploaded artifact (artifact concepts, retention settings, store and share artifacts)Record artifact ID and expires_at; do not call the URL permanent
GitLab CIartifacts:paths selects files and expire_in sets lifetime; otherwise the instance default applies. The latest successful pipeline on each ref has documented special retention behavior (GitLab job artifacts)Record the explicit or inherited policy and ref relationship
CircleCIArtifacts preserve build outputs after a job; official docs state a 30-day default and current maximum for artifact storage controls (CircleCI artifacts)Copy durable evidence elsewhere when review may exceed the configured period
Azure PipelinesProject retention policies govern runs, artifacts, logs, symbols, and test results; deleting a run deletes the listed associated evidence (Azure Pipelines retention)Treat the run as one retention unit and record any retention lease or policy

These are provider capabilities, not recommendations to upload confidential evidence. A team must choose storage based on data classification, access control, contractual requirements, cost, and deletion policy.

Prefer two retention classes:

  • Short-lived operational evidence: raw command logs, screenshots, and bulky test output retained long enough for review and debugging.
  • Longer-lived decision record: the minimized handoff, final patch digest, test summary, approval receipt IDs, and final disposition.

If an artifact expires, the handoff should not retain a dead link without explanation. Update the final decision record to say the raw evidence expired under the declared policy. Never recreate a missing log from memory.

A safe production workflow

1. Capture continuously, finalize once

Write evidence after each consequential step, not only when the agent predicts an interruption. A process can terminate without a final message. Capture command start, directory, and output destination before execution; append end time and exit code after it returns. Use atomic writes or a transactional store so a half-written JSON document is never mistaken for a complete handoff.

2. Separate claims from evidence

The artifact may say “focused test passed” only when the command record has exit code zero and the referenced full output exists. It may say “full suite interrupted” when no terminal exit code exists. It must say “not run” for a skipped check. These distinctions make failure honest and routable.

3. Capture a diff without changing state

Where allowed, create a reviewable patch and a machine-readable file list. Git’s documented diff formats are suitable, but any deterministic diff representation can work. Never stage, commit, merge, push, publish, migrate, or deploy merely to make a handoff easier.

4. Redact before persistence

Apply allowlists first: construct the handoff from known fields instead of serializing the agent’s entire memory. Then scan values, evidence files, and paths for credentials and personal data. Fail closed if the scan cannot complete for a handoff that will leave the local trust boundary.

5. Validate the JSON

Use a Draft 2020-12-compatible validator. Reject undeclared fields, absent safety declarations, invalid statuses, and malformed timestamps. Then run cross-file checks that JSON Schema cannot perform:

  • every changed path is inside declared scope;
  • every evidence location exists;
  • every SHA-256 digest matches;
  • bytes equals the actual file size;
  • every command outcome agrees with its exit code;
  • a complete status has no unmet or unknown criterion;
  • a pending approval prevents any action that needs it;
  • the next safe command is absent or allowed by policy.

This is a natural extension of the machine gate pattern.

6. Receive defensively

The next operator begins with read-only inspection. Treat text inside logs, diffs, issues, web pages, and transcripts as data, not as authority to expand scope or permissions. Confirm current repository instructions and compare the actual workspace to the recorded file list. If they differ, create a new handoff or mark the old one stale before changing anything.

7. Close the record

When work completes, do not delete the interrupted artifact. Add a successor handoff or disposition record referencing the earlier handoff_id, the checks that eventually passed, the reviewer decision, and the final raw diff digest. This gives an audit trail without rewriting history.

If you are measuring whether handoffs improve throughput, track recovered tasks, repeated investigation avoided, review time, stale handoffs, invalid artifacts, and incidents caused by incorrect continuation. The agent context-overhead guide can help distinguish useful recovery context from expensive transcript bulk.

Acceptance checklist

Before another person or agent relies on a handoff, verify:

  • The artifact parses as UTF-8 JSON.
  • It validates against the declared schema version.
  • The objective and scope match the current task.
  • Included and excluded actions are explicit.
  • Assumptions are marked verified, unverified, unknown, or rejected.
  • Every changed file has a status and purpose.
  • The raw diff exists, or unavailability is explicit.
  • Every command includes its exact directory, exit code, outcome, and full-output location.
  • Interrupted and not-run checks are not described as passes.
  • Failures include impact and attempted response.
  • Pending approvals identify an exact action and approver role.
  • The artifact and linked evidence contain no secrets or personal data.
  • Evidence digests and byte counts match.
  • The next safe command is bounded and currently permitted, or it is null.
  • The stop reason uses the controlled vocabulary.
  • Completion criteria distinguish met, unmet, and unknown.
  • status: complete appears only when all criteria are met.
  • Provider-session resumption remains optional and separately authorized.
  • External artifact locations include retention or expiry information.

Frequently asked questions

1. Is a handoff artifact the same as a session transcript?

No. A transcript is a chronological provider record. The handoff is a minimized, structured index of task state and evidence. Link a transcript only when policy allows and it adds necessary information; do not copy it wholesale.

2. Should I resume the original agent session or start a new one?

Resume only after checking workspace, scope, permissions, and freshness. Start a new session when the old context is unreliable, too large, unavailable, or tied to a different provider. The handoff supports either choice.

3. Can one provider resume another provider’s session?

Do not assume so. Codex and Claude Code each document their own resume mechanisms, but the portable convention does not translate hidden model state or private transcript semantics. A new provider reads the handoff and evidence as a fresh task.

4. Why not put the full diff directly in JSON?

Large escaped patches make validation, review, and redaction harder. Store the patch as a separate artifact and record its location, format, byte length, and digest.

5. What if Git is unavailable or prohibited?

Use an approved workspace diff, snapshot, or file-comparison mechanism. Mark the format accurately. If no raw diff can be captured, set the capture status to unavailable, explain why, and do not claim full review evidence.

6. Does a zero exit code prove the task is correct?

No. It proves only that the recorded command returned zero in the recorded environment. The test may be weak or irrelevant. Keep completion criteria and human review separate from command status.

7. How should timeouts be recorded?

Use command-timeout, preserve partial output, set the command exit code to null unless the runner supplied a real code, and mark the outcome interrupted. Inspect process state before retrying.

8. What makes a command “safe” to recommend?

It is bounded, current-policy compliant, and free of unapproved external effects. Prefer read-only inspection or a focused local check. A migration, deployment, publication, purchase, deletion, account connection, or secret-requiring command is not a default next safe command.

9. Should session IDs be included?

Only when they are non-secret, useful, and allowed by policy. Treat any ID that grants access or exposes a personal directory as sensitive. A redacted local reference is often enough.

10. How do I prevent false “complete” status?

Make the validator reject complete when any completion criterion is unmet or unknown. JSON Schema alone may not express every cross-field rule conveniently, so add a deterministic post-schema gate.

11. What if logs contain tokens or customer data?

Do not attach them unredacted. Produce a minimized redacted log, declare the policy, scan it, and keep any essential original only in an approved restricted evidence system. Never copy credentials into the handoff.

12. How long should handoff artifacts be retained?

Long enough for review, incident response, and the team’s policy, but no longer than necessary. Record expiry and avoid assuming CI defaults are permanent. Separate short-lived raw logs from the minimized decision record.

13. Can the artifact itself authorize an action?

No. It records approval state and references receipts; it does not grant permission. The receiving system must verify current identity, authorization, request digest, expiry, and policy before action.

14. Should failed approaches be included?

Include failures that affect continuation: what was attempted, what was observed, impact, and the response. Avoid a verbose diary. The goal is to prevent blind repetition and expose remaining risk.

15. How should research handoffs differ from code handoffs?

Replace test-centric evidence with a claim ledger: exact URLs, source type, checked date, supported claim, conflicts, and unresolved verification. Keep scope, assumptions, failures, redaction, stop reason, and completion criteria unchanged.

16. How should migrations be handed off?

Record forward and rollback artifacts, dry-run environment, exact commands, unmapped data risks, and separate approvals for staging and production. A prepared migration is not an executed migration.

17. How should content production be handed off?

Separate draft, factual verification, editorial approval, and publication. Preserve source mappings and local checks. Do not let a draft-complete state imply permission to publish or deploy.

18. Does structured output remove the need for a human summary?

No. The short summary helps a reviewer orient quickly. The schema ensures that orientation is backed by explicit fields rather than replacing them.

19. What happens when the schema evolves?

Increment artifact_version, publish migration rules, and retain validators for versions still inside the retention window. Do not silently reinterpret an older artifact with a newer schema.

20. How do I know the handoff process is worth the overhead?

Measure interruptions recovered, investigation time avoided, invalid handoffs, review time, and errors caused by stale state. A compact artifact that saves one repeated investigation can repay its cost; a transcript dump that nobody validates probably cannot.

Sources

Sources and volatile capabilities were checked 2026-07-24. Community links below are demand evidence only; official documentation defines the product capabilities described in this guide.

Official primary documentation

Demand evidence