Skip to content

.shipper directory

Shipper stores repository configuration and stage I/O in a checkout’s .shipper/ directory. It stores machine-level data for the current operating-system user under the Shipper home, which is ~/.shipper/ by default; the SHIPPER_HOME environment variable relocates that entire tree so a second, independent Shipper instance can run in the same account. Paths on this page use the default location. Some repository-local entries are shared team configuration that should stay trackable in git. Other entries and all user-home storage stay outside version control.

Bare shipper setup and bare shipper setup --assist do not create repository .shipper/ artifacts by default. See Settings for the settings those commands manage outside the checkout. Use --repo to opt into committed .shipper/ files for a GitHub-linked repository. Assisted setup may leave an explicitly selected bootstrap hook in an unlinked working tree for manual review and commit.

Committed .shipper/ entries should remain visible to git. Runtime ignores belong in .shipper/.gitignore, not in a repository rule that hides the whole .shipper/ directory.

  • Purpose: Shared repository configuration for Shipper.
  • Created or updated by: shipper setup --repo writes committed intent rather than copying all built-in defaults. A fresh file contains the repository cliVersion and an installCommand only when setup discovered or explicitly confirmed one. Manage other shared settings—including a deliberate agent pin—explicitly with shipper settings set <key> <value> --repo or shipper settings unset <key> --repo.
  • Version control: Committed. See Settings for the setup contract, schema, scopes, and precedence.
  • Purpose: Ignored, unused runtime filename. Shipper never reads it as settings and emits a one-line ignored-file warning when it exists.
  • Created or updated by: Shipper does not create or update this file.
  • Version control: Ignored by the scaffolded .shipper/.gitignore; not committed and not a settings layer.
  • Purpose: Executable hook scripts that run around Shipper stages and worktree lifecycle events.
  • Created or updated by: shipper setup --repo creates the directory on disk. Users add executable hook scripts. Bare shipper setup --assist does not create hooks by default; it may create .shipper/hooks/worktree-setup only after explicit opt-in to a committed bootstrap-hook fallback when one installCommand cannot represent the repository bootstrap. For an unlinked user-level setup, that opted-in hook is left for manual review and commit rather than setup-PR finalization.
  • Version control: Not ignored by Shipper. Commit hook scripts when they are shared automation. .shipper/hooks/worktree-setup is a commit-trackable repository artifact when chosen. An empty directory is not represented in git. Hook names and semantics belong in Configure hooks.
  • Purpose: Committed scaffold-managed helper scripts used by Shipper runs.
  • Created or updated by: shipper setup --repo creates the directory and writes helper scripts, currently including .shipper/scripts/install-deps.sh. Every stage worktree uses Shipper’s current bundled copy of install-deps.sh, so hand-editing the committed copy has no effect on dependency installation. Set installCommand to customize what the helper runs.
  • Version control: Committed shared helpers. These files are covered by the setup —repo drift guard.
  • Purpose: Temporary caches and scratch data, including the private XDG/tool-state cache, shared-cache fallbacks, and worktree-local caches created by configured commands. Shipper-owned npm and Turbo caches normally live in the user-home repository cache described below.
  • Created or updated by: shipper setup --repo creates the directory. Shipper and configured commands may write temporary data there during local runs.
  • Version control: Gitignored by .shipper/.gitignore; not committed.

Sandboxed temporary storage is sandboxed-only and is not created under .shipper/tmp/. On macOS, Linux, and WSL2, a sandboxed Codex child receives a Shipper-managed unique real /tmp/shipper-cx-<24 lowercase hex> directory (shown as /private/tmp/... after macOS canonicalization). Sandboxed Claude uses native per-session temporary storage with no Shipper TMPDIR, TMP, or TEMP override. Unsandboxed runs and other agents retain their existing temporary environment. There is no setting, flag, or opt-out.

  • Purpose: Per-stage context files that Shipper provides to agents.
  • Created or updated by: shipper setup --repo creates the directory and writes input/.gitkeep. Runtime code writes context files there during stage runs.
  • Version control: input/.gitkeep is committed so the directory exists in fresh checkouts. All other input/* files are ignored by .shipper/.gitignore.

Representative runtime files include pr-diff.patch, pr-files.json, pr-metadata.json, review-threads.json, ci-status.json, ci-log-*.txt, pass-info.json, dependencies.md, conflict-context.txt, push-error.txt, install-error.txt, groom-cross-issue-index.json, groom-issue-details/local-*.json, groom-issue-details/github-*.json, and groom-post-flight-failure.txt. An oversized frozen grooming PR-overlap snapshot conditionally spills to groom-pr-overlap.md; it is gitignored, absent from a clean worktree, not always present, and not the durable source of truth. This list is not exhaustive.

The single groom cross-issue index references both detail-file patterns. Every other live Shipper-managed issue, at every open workflow stage, gets a local-*.json detail; mirror-on unadopted GitHub-only entries use github-*.json. The issue being groomed, closed managed issues, and tombstones are absent from both the index and the details directory, which is rebuilt from scratch on every prepared run. Every index entry carries the same bounded scopeExcerpt skim of at most 512 body characters, and Shipper creates each detail file with exactly { "body": string }. A managed candidate’s current title, current stage, and current blockers stay in its index entry, and its accumulated workflow-event history is never supplied. In mirror-on output, unmanagedGithubSweep records a complete (possibly truncated) sweep or an unavailable managed-only fallback. Store-only output omits that property and performs no unmanaged sweep or adoption-origin lookup. The current issue’s own launch facts — its recorded blocker baseline — travel in groom-recorded-blockers.json instead. These index and detail files are created for stage execution under ignored input/*; they are runtime context, not tracked configuration.

  • Purpose: Stage protocol outputs written by agents for Shipper to consume.
  • Created or updated by: shipper setup --repo creates the directory and writes output/.gitkeep. Agents write result, comment, and payload artifacts there during stage runs.
  • Version control: output/.gitkeep is committed so the directory exists in fresh checkouts. All other output/* files are ignored by .shipper/.gitignore.

Representative runtime files include result.json, comment-<number>.md, grooming-comment-<number>.md, groom-<number>.json, blocked-comment-<number>.md, issue-draft.json, issue-body.md, issue-body-<number>.md, child-<number>-1-body.md, pr-body-<number>.md, pr-spec-<number>.json, review-payload-<number>.json, and replies/<comment-id>.md. This list is not exhaustive.

During shipper new, the agent writes result.json, issue-draft.json, and issue-body.md. After Shipper validates the draft, writes the local issue-state record, and creates the GitHub mirror, Shipper overwrites result.json with the final created_issue record.

  • Purpose: Short local orientation for someone opening .shipper/ in an initialized repository.
  • Created or updated by: shipper setup --repo writes the file from Shipper’s README template.
  • Version control: Gitignored by .shipper/.gitignore; not committed.
  • Purpose: Runtime ignore rules for generated .shipper/ files.
  • Created or updated by: shipper setup --repo writes the file.
  • Version control: Committed.

The current runtime ignore contract is exactly:

tmp/
settings.local.json
README.md
input/*
!input/.gitkeep
output/*
!output/.gitkeep

Core’s src/lib/shipper-user-paths.ts is the single place Shipper-owned user-home locations are resolved. New Shipper storage roots belong there. The complete current inventory is:

  • ~/.shipper/config/settings.json stores user-global settings.
  • ~/.shipper/config/<storageKey>/settings.json stores per-repository personal settings.
  • ~/.shipper/issue-state/ is the issue-state enumeration root; ~/.shipper/issue-state/<storageKey>/ stores a repository’s issue history, workflow state, locks, groom-output recovery bundles, and durable question sessions. Saved sessions live at groom-question-sessions/s<localId>.json, survive CLI and MCP host restarts, and have no age-based expiry. Their version 1 record may contain ownerSurface: "desktop" | "mcp" | "terminal". Absence means the legacy session is unowned; readers never infer or backfill an owner. The same schema version may contain optional prOverlapEvidence.active plus a one-hop supersedes receipt. Legacy absence is normalized, only after ownership is established, to an unavailable active snapshot explaining that the session predates overlap capture. Exactly one active snapshot and at most the immediately prior capture and supersession timestamps are retained—never old paths, overlap rows, or a snapshot history. Explicit refresh preserves the conversation, owner, creation time, and original issue fingerprint while replacing only prOverlapEvidence and updatedAt.
  • ~/.shipper/sessions/<storageKey>/ stores agent sessions and usage metadata. See Session metadata for what a session sidecar records.
  • ~/.shipper/auto-ship/<storageKey>/state.json stores the combined Auto-ship desired state and durable safety-stop latch/history for one canonical repository.
  • ~/.shipper/auto-ship/<storageKey>/dispatch-owner.json is the machine-global, per-repository live-dispatcher record within one Shipper home. Its sibling dispatch-owner.lock guards atomic mutation by Terminal auto-ship, Desktop, and the daemon.
  • ~/.shipper/run-history/<storageKey>/records.json stores repository-identity-family records and repository-address-family records whose scope includes that resolved repository identity, so one storage-key document may contain both families. The fallback ~/.shipper/run-history/addresses/<addressKey>/records.json stores repository-address-family records while their scope is unresolved. Run scope selects the family: the repository-identity family requires issue context, while the repository-address family permits nullable issue context for pre-identity and local-address runs. The storage-key document’s sibling ~/.shipper/run-history/<storageKey>/records.lock and the fallback address path’s sibling records.lock serialize mutations.
  • ~/.shipper/repos/ stores managed repository clones.
  • ~/.shipper/repository-access/<sha256("named\0" + normalizedOwnerRepo)>/ stores the repository-scoped cross-process managed-clone lease. Its state.lock serializes lease-record mutations; writer-intent.json expresses writer preference, and token-named records under readers/ admit simultaneous readers. This is per repository rather than one machine-wide setup lock.
  • ~/.shipper/worktrees/ stores ephemeral stage worktrees.
  • ~/.shipper/worktree-owners/ stores durable ownership records used by worktree reclamation and the machine-wide orphan sweep of that Shipper home.
  • ~/.shipper/license/ stores the current user’s Shipper license and credentials for that Shipper home; a relocated instance signs in separately.
  • ~/.shipper/cache/labels/ caches successful Shipper label checks.
  • ~/.shipper/cache/shared/ is the active machine-wide shared-cache root of that Shipper home.
  • ~/.shipper/cache/shared/npm/ stores Shipper-owned npm package-cache data.
  • ~/.shipper/cache/shared/turbo/ stores Shipper-owned Turbo task-cache data.
  • ~/.shipper/cache/shared/uv/ stores Shipper-owned uv cache data.
  • ~/.shipper/cache/shared/pnpm/ stores the Shipper-owned pnpm content-addressed store.
  • ~/.shipper/cache/shared/cargo/ is the complete Shipper-owned Cargo home, including its package cache, installed binaries, and install ledgers.
  • ~/.shipper/cache/shared/playwright/ stores Shipper-owned Playwright browser binaries and tools.
  • ~/.shipper/cache/shared/electron/ stores Shipper-owned Electron runtime archives.
  • ~/.shipper/cache/shared/electron-builder/ stores Shipper-owned electron-builder packaging and signing-adjacent toolsets.
  • ~/.shipper/cache/repositories/ contains orphaned per-repository cache directories from older Shipper versions. Normal execution leaves them untouched; only explicit shipper cache clear removes the entire legacy root.
  • ~/.shipper/recovery/replies/ preserves output-protocol replies that could not be delivered.
  • ~/.shipper/run/daemon.sock is the per-user daemon endpoint (one daemon per Shipper home); ~/.shipper/run/daemon.owner.json is its fenced ownership record, and ~/.shipper/run/daemon-status.json is advisory process and service state.
  • ~/.shipper/current is the installer’s atomic symlink to the current immutable CLI generation under ~/.shipper/cli/<version>-<generation>/shipper-cli; ~/.shipper/current/bin/shipper is the launcher MCP and Desktop use to start the daemon for that Shipper home.
  • ~/.shipper/logs/ stores CLI ship logs and shared supervision diagnostics at ~/.shipper/logs/daemon.log, ~/.shipper/logs/daemon.log.1, and ~/.shipper/logs/daemon.log.2.
  • ~/.shipper/pause-sentinels/ stores host-owned, per-run pause signal files.

The personal-settings, issue-state store, sessions, and auto-ship locations are repository-keyed. Those four locations use the same stable repository identity and storage key. Named repositories keep the existing flat owner-repo key. Repositories without a determinable name use unlinked/<lineage-token>. Every worktree of one repository shares the same key. Run history accepts that stable repository scope and a repository-address scope so a path-keyed local repository or pre-identity Init run has durable history before canonical identity exists.

Every repository and worktree for the current operating-system user shares the same eight child paths under ~/.shipper/cache/shared/; cache preparation does not resolve or depend on repository identity. On POSIX, Shipper creates the root and children with owner-only 0700 permissions and uses 0600 probe files. It grants supported agents this one common root rather than the broader ~/.shipper/cache/ directory.

Shipper adds no lock or per-run slot. npm, uv, Playwright, Electron, and electron-builder already use machine-global cache locations for unrelated projects; pnpm’s machine-global content-addressed store is shared within the same operator/trust domain; Cargo applies its package-cache process locks; and Turbo keys entries by task inputs and supports sharing across repositories and machines. Tools may perform their own cache maintenance, but Shipper never automatically evicts, prunes, or size-caps active or legacy cache data. First use warms through ordinary tool downloads; Shipper does not seed from operator caches. Because the whole Cargo home is machine-wide and agent-writable, cargo install results, the install ledger, and executable cargo-* subcommands are also shared across unrelated repositories. Set worktreeEnv.CARGO_HOME to an operator-owned path when that surface needs isolation.

Normal worktree preparation, install recovery, target reclamation, and orphan sweeps cannot reach either shared-cache root. shipper cache clear is the explicit maintenance action: it removes both ~/.shipper/cache/shared/ and the full legacy ~/.shipper/cache/repositories/ tree and therefore affects every repository on the machine. It does not touch sibling cache namespaces such as cache/labels, worktree-private caches, or operator-configured paths.

~/.shipper/run/daemon.owner.json is the daemon’s fenced ownership authority. Its process liveness and generation token control reclamation and cleanup. A live owner and an owner whose liveness is indeterminate are both retained indefinitely; elapsed age never authorizes reclamation. ~/.shipper/run/daemon-status.json is a token-matched advisory snapshot for inspection when the endpoint does not respond. Its PID and other fields never authorize signaling or reclamation. Current supervision-delivery health is live-only. It is not stored in daemon-status.json, and Shipper does not create a companion delivery-health file. An unreachable daemon therefore reports delivery health as unknown rather than replaying or inferring a stale healthy value.

The daemon runtime directory is owner-only mode 0700. The Unix socket, owner record, advisory status record, and diagnostic files are owner-only mode 0600. Shipper tightens only daemon-owned runtime state and does not chmod the wider ~/.shipper/ home or general ~/.shipper/logs/ directory.

Supervision diagnostics contain the existing daemon lifecycle events plus versioned supervision-recovery-phase records from both Desktop and daemon hosts. Recovery records have schemaVersion: 1, a recovery ID, host (desktop or daemon), phase, transition, phase-start and event timestamps, and completion time plus elapsed milliseconds when a phase completes. Optional fields identify repository address, address key, and identity; a proven-live file-mutation-guard blocker; or a one-based background-service connection-attempt ordinal and outcome. Connection attempts, run-history recovery, aggregate and per-repository checkout preparation, dispatch-ownership acquisition, the first fill, first snapshot, and the phase active at a local overrun are retained on this same diagnostic surface. Desktop and headless supervision therefore use one schema rather than divergent host-specific records.

Schema-v1 startup-decision is the sole current record for a final direct startup decision. It covers starts, compatible and incompatible peers, every uncertainty, concurrent convergence, and every refusal. Each record stores the coarse outcome (started, already-running, or refused), the reason discriminator, and only an applicable connect or refusal code. New starts also include daemon instance, version, and generation; refusals may include internal detail. One terminal decision produces one matching record. Historical rotated records that used earlier unversioned start or refusal event names remain readable and are not migrated.

The same rotating log stores schema-v1 supervision-delivery-degraded, supervision-delivery-restored, and supervision-delivery-rejected records. Each includes daemon and client identity, event time, sorted affected delivery scopes, outstanding age, and outstanding count. Degraded records preserve the oldest acceptance time; restored records preserve the pre-resolution oldest delivery, before/after counts, and whether completion, unsubscribe, subscription replacement, or connection removal resolved the episode. Rejections include one bounded error summary. Payloads, cursors, delivery IDs, and accepted/completed progress positions are excluded. A connection produces exactly one degraded start and one restored/removal record per episode, while promise-chain rejection is coalesced once per connection. These files remain available after detached daemon startup even when console output is unavailable.

Rotation is fixed and non-configurable: the active daemon.log plus at most daemon.log.1 and daemon.log.2, each no larger than one MiB, for an aggregate bound of three MiB. A transient daemon.log.lock serializes rotation and append across processes but is not a retained diagnostic file. Routine negotiation, ping, service-information, command, and subscription traffic is not logged. These records add no operator-facing diagnostics surface.

~/.shipper/auto-ship/<storageKey>/state.json is a complete version 6 JSON record. It is machine-global for the current operating-system user’s Shipper home: every Shipper process in that account that resolves the same Shipper home shares it, it survives process and machine restarts, and it is not synchronized to other Shipper homes, operating-system users, or machines. This is durable runtime state, not committed configuration and not part of the shipper settings surface. Halt, disable, and the safety stop are three distinct states. haltRequested is a persistent operator-requested repository hold that preserves enabled and workerCount, survives an idle repository, safe-checkpoint wind-down, and process or machine restart, and ends only through an explicit clear. enabled: false is disable: it drains current automatic work and prevents dispatch until Auto-ship is enabled again, and neither disabling nor enabling clears a halt. safetyStop is the automatic failure safeguard with its own latch and its own resume gesture, and safety recovery never clears a halt. Each machine honors and clears its own record independently. Its shape is:

{
"schemaVersion": 6,
"repository": { "kind": "named", "repo": "owner/repo", "storageKey": "owner-repo" },
"enabled": true,
"workerCount": 2,
"haltRequested": false,
"enablementGeneration": 1,
"launchFailureRetries": {
"presentedAt": null,
"lastEpisodeAttemptAt": null,
"repository": null,
"issues": {}
},
"worktreeSetupContentionAttempts": { "355": 2 },
"safetyStop": {
"accountingGeneration": 0,
"progressGeneration": 2,
"engagementCount": 1,
"pendingConditions": [],
"current": {
"engagedAt": "2026-08-07T12:00:00.000Z",
"failures": [
{
"scope": "issue",
"kind": "terminal-stage-failure",
"conditionType": "issue-stage:agent-exit-exhausted",
"localId": 311,
"githubIssueNumber": 1751,
"title": "Example failure",
"revision": "[\"shipper:implementing\",\"2026-08-07T11:00:00.000Z\",\"agent-exit-exhausted\"]",
"summary": "Stage execution failed (agent-exit-exhausted)",
"cause": "agent exited unsuccessfully",
"progressGeneration": 0,
"relation": "initial"
},
{
"scope": "repository",
"kind": "terminal-stage-failure",
"conditionType": "rewritten-ref",
"objectKey": "refs/heads/main",
"revision": "[\"old-object\",\"new-object\"]",
"summary": "Rewritten repository ref refs/heads/main",
"cause": "base ref changed",
"progressGeneration": 1,
"relation": "condition-changed"
},
{
"scope": "issue",
"kind": "pending-checks-exhausted",
"conditionType": "pending-checks",
"localId": 313,
"githubIssueNumber": 1753,
"title": "Example check exhaustion",
"revision": "[77,\"head-revision\",[\"build\",\"test\"]]",
"summary": "Pending checks exhausted for PR #77",
"cause": "checks timed out",
"progressGeneration": 2,
"relation": "recurred-after-progress"
}
]
},
"previous": null
}
}

repository must exactly match the canonical identity that selected <storageKey>. Each non-null engagement has an ISO engagedAt timestamp and exactly three complete qualifying condition instances. accountingGeneration, progressGeneration, and engagementCount are non-negative safe integers. pendingConditions durably retains strikes from the first accepted condition until the threshold engages. current is the engaged latch; previous retains exactly the last engagement that was cleared. Later cycles replace that one previous entry, while the monotonic lifetime engagement count (engagementCount) continues to show recurrence. There is no engagement array, audit ledger, age pruning, or expiry.

Version 1 through version 5 desired-state records remain readable: their desired values are projected with the requested canonical identity, an empty pending ledger, and progress generation zero. Retained historical engagement tuples migrate to explicit legacy-unknown entries so every surface reports that condition identity is unavailable instead of inventing independent issue conditions. The next real mutation performs the lossless migration to a complete version 6 record and reports it; an exact no-op against a current version 6 record performs zero writes. Version 6 validation is strict and whole-record: an unknown field or version, malformed safety value, invalid condition or timestamp, or invalid counter makes a malformed known-version record recoverable corruption. Such a record may be replaced by a successful guarded mutation, while unreadable storage remains a separate unavailable inspection result. Old launchFailureNotice bytes are intentionally discarded because they cannot establish retry scope, posture, per-scope count, deadline, or recovery baseline.

An unknown positive schema version is instead an unsupported-schema-version compatibility conflict, and a valid version 2, 3, 4, 5, or 6 record whose embedded repository does not exactly match the canonical identity is a repository-identity-mismatch compatibility conflict. Those authoritative records remain byte-for-byte intact and refuse mutation. Shipper never projects compatibility conflicts as disabled, one worker, no halt, unsuppressed, or safe, and never offers repair, reset, or discard as a remedy.

All ten writers—desired-state patch, ordered safety-event accounting, dedicated safety resume, automatic launch-failure recording, launch-failure retry consumption, successful-launch clearing, launch-failure retry reconciliation, launch-failure acknowledgement, and worktree-setup contention record/clear—use the sibling ~/.shipper/auto-ship/<storageKey>/state.lock and atomically replace the whole record. Readers therefore observe either the old or new state, different-field updates are preserved, and write or lock failures are reported to the caller. CLI and supervisor submit qualifying conditions and successful advancements through the same locked API. One unchanged logical condition and revision in one progress generation is recorded once regardless of observer; a changed revision, A → B → A, or the same revision after progress appends a new strike. A contender whose accounting generation was fenced by a clear receives stale-generation and the newer snapshot instead of repopulating the old epoch. A failed accounting write publishes no partial ledger, latch, or success. A compatibility conflict prevents every writer from modifying the authoritative bytes.

Exactly two atomic state transitions clear current: a genuine durable not-enabled-to-enabled transition, or the dedicated safety-resume operation. Both move the cleared engagement to previous, increment accountingGeneration, and clear the prior epoch’s pending conditions. Re-enable clearing retains the desired patch semantics, which merge only the fields a patch supplies; an outstanding halt is preserved unless the same patch explicitly sets haltRequested: false. Dedicated resume preserves enabled, workerCount, and haltRequested exactly. Repeated enabled: true, disabling, worker/halt-only patches, manual success, elapsed time, and process or daemon restart do not clear the latch. Engagement itself also preserves all desired-state fields.

The terminal shipper auto-ship group and MCP use these same guarded operations. Terminal and MCP enable have identical desired-state semantics; both retain the resolved local checkout in the registry’s existing sources.mcp wire partition. MCP enable additionally makes a bounded attempt to provide a supervisor, while terminal enable starts and contacts none. Terminal enable reports that registration makes the checkout supervisable and visible to Desktop and the daemon. Disable, drain, clear-halt, and resume never remove that discoverability, and no gesture in the group removes it.

A halt is released only by an explicit clear: shipper auto-ship clear-halt, the Desktop Clear halt action, or the MCP tool shipper_clear_auto_ship_halt, each of which performs one { haltRequested: false } patch. Clearing releases repository-control checkpoint Stops a supervisor has not yet consumed while enablement is also open, but never restarts already-stopped runs, restores withdrawn queue entries, or replays prior work. Explicit manual advances and per-run Pause and Stop actions remain available while a repository is halted, and enabled Auto-ship stays non-dispatching until the halt is cleared. Draining, Halted, and unknown-runtime Halt requested are presentations derived from observed runtime evidence, not persisted fields, and no surface reports a halt as consumed or automatically cleared.

A committed stopped repository with no sessions or other deadlines remains enabled and keeps the daemon resident, but its process-local supervisor can become remotely quiescent while preserving the durable stop. Normal idle exit requires zero enabled repositories. A later daemon or other supervision host reloads current before candidate inspection, projects the retained facts directly, and remains suppressed. A failed local engagement proposal is not yet settled, so idle exit cannot erase it before persistence is retried.

This version 6 contract explicitly supersedes the former process-local, run-scoped, and daemon-lifetime safety-stop storage contract, while retaining the same operating-system-user and machine scope.

Guard acquisition and atomic replacement briefly create .pending-* and .tmp sidecars in the record directory. Normal and error cleanup removes them; leftovers from a force-quit are ignored by reads, are harmless, and are safe to delete. Later file-mutation guard activity automatically reclaims recognized guard remnants with confirmed-dead owners in that same directory. This reclamation does not apply to atomic-replacement .pending-* or .tmp files. Other candidates remain harmless, and guard remnants may remain indefinitely when no later guard activity occurs in that directory.

~/.shipper/auto-ship/<storageKey>/dispatch-owner.json identifies the Shipper process currently dispatching work for one repository. This machine-global, per-repository version 1 record contains the canonical repository identity, owner surface (terminal, desktop, or daemon), PID, instance ID, acquisition timestamp, and internal renewedAt timestamp. Public status calls that final field Last refresh (diagnostic context only). It is never a safety decision. The runtime interprets a recognized Terminal instance marker on a pre-upgrade Desktop-form claim as the semantic terminal surface. That old record remains occupied and byte-for-byte unchanged until its drain’s exact normal release; a new Terminal drain writes terminal directly.

Terminal auto-ship, Desktop, and the Shipper daemon all publish and remove this record. Every surface serializes mutation with dispatch-owner.lock, atomically rechecks the current holder, and normally releases only its own exact PID-and-instance ownership. Only process liveness controls ownership: running and could-not-determine holders remain held, while a proven-dead known holder is treated as not held. Dispatch ownership has no timeout and is never removed automatically because of a timestamp.

Readers first validate a stable holder-identity envelope. A readable future version or unknown surface with canonical repository identity, PID, instance ID, and valid timestamps remains an opaque held record and is reported as an unrecognized holder. Only a proven-dead PID makes it not held. An identifiable holder with canonical repository identity and positive PID remains exclusive even when an ancillary field such as a timestamp or instance ID is invalid or missing. While its PID is running or liveness cannot be determined, dispatch is blocked, public ownership is unavailable, and no clearing gesture is offered. Only proven death permits replacement. Invalid JSON, missing or invalid PID, incomplete repository identity, and noncanonical or wrong-repository identity are treated as absent. A real filesystem read failure is also public unavailable status and does not authorize dispatch or offer a clearing gesture.

All operator surfaces project exactly five holder facts: surface, PID, acquisition timestamp, liveness, and last refresh. Liveness reads either The holder is running. or Shipper could not determine whether the holder is still running. While dispatch ownership is held, any other scheduler on this machine stands down rather than dispatching for this repository.

The interlock is repository-local: a holder for one storage key does not block another repository. For one repository, the first running or could-not-determine Terminal, Desktop, or daemon holder wins and at most one supported automatic drain runs. Supervisors retain their ownership while an enabled repository is remotely quiescent. If Terminal already owns it, only new dispatch for that repository is deferred; in-flight and non-dispatch work continue, and the supervisor retries on its normal control cadence after Terminal releases the exact ownership.

A Terminal refusal occurs before sequential or parallel selection, attempts no issue, and exits 80. Use shipper status to inspect the condition. The three deliberate recovery gestures are Desktop’s Clear dispatch ownership, shipper auto-ship clear-dispatch --force, and the two-call MCP tool shipper_clear_dispatch_ownership. Each previews first. Discarding dispatch ownership held by a genuinely running holder means two schedulers will dispatch for this repository concurrently.

For a Desktop or daemon holder, shipper auto-ship disable, Desktop, or MCP can instead record a graceful stop; in-flight work drains before that supervisor releases its ownership. For a Terminal holder, wait for the drain or interrupt its PID. For an unrecognized holder, wait or stop the originating PID without guessing its surface.

The confirmation token hashes stable repository and holder identity: repository kind, repository, storage key, effective surface, PID, instance ID, and acquisition timestamp. It excludes internal renewedAt, liveness, schema version, and future fields. A normal same-owner refresh or a different liveness observation therefore does not invalidate confirmation; a successor owner does. The compare-and-delete occurs under dispatch-owner.lock, returns changed without deletion when the stable identity differs, and never changes state.json. Clearing one repository leaves every other repository’s ownership and desired state untouched. The affected supervisor observes the removal, reacquires on its normal four-second control cadence, and resumes dispatch without a reload.

Normal shutdown remains separate and releases only the surface’s own understood owner. A headless daemon can remain the recorded holder with no Desktop window open.

The terminal disable, drain, and clear-halt gestures return after recording intent, do not prove dispatch has stopped, and direct the operator to shipper status for confirmation. The terminal resume gesture, Desktop Resume, and shipper_resume_auto_ship all reach the same dedicated safety-resume transition and never clear a halt.

--ignore-dispatch-lease is an unsupported, inspection-only concurrent-operation bypass. The overriding Terminal process publishes no ownership, releases nothing, does not coordinate schedulers or transfer ownership, and gives supervisors no reason to stand down. Desired state remains separate: disabling Auto-ship changes operator intent but neither synchronously kills work nor permits the CLI to remove another holder’s record.

~/.shipper/run-history/<storageKey>/records.json and the address-scoped path above contain two coexisting record families in JSON documents shaped as { "records": [...] }. Run scope selects the family: the repository-identity family requires issue context, while the repository-address family permits nullable issue context so New and pre-identity Init runs are representable. The repository-identity family retains the canonical repository identity. The repository-address family carries a stable repository-address scope and an optional resolved repository identity.

Schema versions 1 and 2 are persisted discriminator details for the repository-identity and repository-address families, respectively. A higher schema number does not mean a newer family, and the withdrawn version 3 establishes no chronology or ordering between the two active families. Mixed-family members are valid coexistence, not unsupported versions. Readers remain compatible with both families. Both families record the unique live sessionId, continuation or retry activityTaskId, command and working directory context, automatic or manual origin, supervisor PID, lifecycle timestamps, terminal status and outcome, exit code where one exists, and durable agent-session log path. A missing log target does not invalidate the historical record because agent-session logs have an independent lifetime.

The lifecycle contains exactly three persisted points: enqueue, spawn, and terminal. An attempt that fails before spawn is terminal failed with a reason and no exit code or command outcome. A vanished supervisor produces the distinct terminal status orphaned. Operator pause and resume signals are transient and do not add lifecycle events; when an attempt exits because of a hold, paused is its terminal outcome. Streamed command output is never stored here—the record links to the durable agent-session log instead. Repository projections are ordered by newest enqueue time and then session ID, and a single-issue projection filters that same stable ordering.

An attended agent cancellation is a distinct spawned terminal status, cancelled. It carries exit code 82 and the validated typed command outcome operator-cancelled, whose prior and resulting stages are identical, issue disposition is leave-in-place, and repository safety is null. Cancellation is neutral history: it is neither the transient pause/resume signal nor the paused terminal outcome. It is also distinct from a supervisor-requested process stop, which retains the legacy cancellation metadata on a failed process result. A bare exit 82 without the typed outcome is not trusted as operator cancellation and remains failed.

Reads validate members independently and tolerate unknown fields at every nesting level. Malformed members and unsupported schema versions are skipped without deletion while one aggregate warning lists the skipped member indexes and versions. Valid lifecycle rewrites preserve additive unknown fields, malformed members, and unsupported members. Invalid top-level JSON or an unreadable records wrapper returns one visible explanation; mutations refuse to replace a document whose raw members cannot be preserved.

Mutations use records.lock as a cross-process guard and atomically replace the complete records.json, so a concurrent reader observes either the complete old document or the complete new document. The only persisted transitions are enqueue, spawn, and terminal, and exact transition retries are idempotent.

Retention applies three fixed, non-configurable bounds together: at most 20 records per issue, at most 500 records per repository, and terminal records no more than 30 days old. Eligible terminal records are removed oldest first. Non-terminal records and records whose supervisor is alive or whose liveness is indeterminate are protected regardless of age. Invalid and unsupported members are also protected. Protected members count toward the caps, so safety can make a cap temporarily unattainable.

Orphan reconciliation uses only supervisor PID liveness. A dead supervisor changes a non-terminal record irreversibly to orphaned with reason supervisor-exited; alive and indeterminate supervisors are left unchanged. Elapsed time never implies that a supervisor is dead, and reconciliation never changes an already terminal record.

The production supervision runtime writes enqueue, spawn, and terminal transitions for New, Ship, Next, Init, Unblock, and Merge. Startup invokes orphan reconciliation rather than inferring process death from age. Current sessions and activity tasks hydrate on attachment. Durable orphaned records retain read-only Interrupted outcomes rather than failures or safety events. Associated issue details, repository setup, or New Issue placeholders can show available evidence; not every saved record is browsable in Desktop. Read-only records gain no stop, retry, or resume controls. Retiring the Activity pane does not purge saved records or transcripts or extend retained output lifetime. New Issue placeholder dismissal, repository un-adoption, and checkout deletion never remove durable run history. There is no purge affordance or configurable retention setting; store-owned fixed retention is the only removal path.

Legacy ~/.shipper/issue-state/_unlinked/ and ~/.shipper/sessions/_unlinked/ directories are not migrated, read through, renamed, rewritten, or deleted.

Each stage session writes a <issue>-<stage>-<timestamp>.meta.json sidecar beside its .jsonl transcript. Two independent sets of model and reasoning-effort facts live there, and they answer different questions.

model and reasoningEffort record what Shipper configured for the run. The "default" literal means Shipper deliberately imposed nothing and left the agent’s own default in place; it has never been a claim about what the agent actually ran. These fields are unchanged.

observedAgentConfiguration records what the agent’s own run output reported, read back from the transcript after the run. It has two members, models and reasoningEfforts, each of which is one of:

  • { "status": "observed", "values": ["…"] } — the identities the run actually revealed, recorded verbatim. Shipper does not canonicalize an alias such as opus to a dated id, strip a provider prefix, or map effort names. More than one value means the session was mixed: the run used more than one distinct model or effort over its life.
  • { "status": "unresolved", "reason": "…" } — no identity could be established, with one of three distinguishable reasons:
    • no-evidence-for-run-mode — the run left nothing readable behind, which is the normal outcome for attended interactive runs.
    • agent-exposes-no-mechanism — the agent never reports this fact at all. A permanent limitation, not a defect.
    • uninterpretable-evidence — evidence was present but could not be read. This one is a fixable resolution defect worth reporting.

The status discriminant plus those three reason literals guarantee no unresolved marker can be mistaken for a real model or effort name by a cohort query.

Two boundaries matter when reading these records:

  • Observed identity describes the main stage session, while the sidecar’s usage totals are whole-session sums that include any subagents the stage spawned. For a subagent-heavy stage running a different tier in its subagents, the model label and the token cost describe different populations.
  • Reasoning-effort cohorts are less trustworthy than model cohorts on agents that expose no effort mechanism — see Supported coding agents.

Determining the observed values never changes which model or effort an agent runs with, never adds a flag, and never fails or delays a run; a failure records an unresolved reason and leaves every other field intact. Sidecars written before this field existed simply omit it and are never migrated, backfilled, or rewritten.

Desktop supplies the canonical ~/.shipper/pause-sentinels/ path through core’s user-path owner. Other hosts may inject a different root without changing supervisor behavior. Shipper creates the selected root with mode 0700 when it is absent. Each running ship receives one path directly beneath it named <hostPid>-<uuid>; the UUID distinguishes generations even if the operating system later reuses a PID. The signal file remains absent until a checkpoint action is requested, is installed or replaced atomically as a complete mode-0600 schema-v1 JSON record, and is passed to the child only through SHIPPER_PAUSE_SENTINEL_FILE. Its origin is exactly operator-pause, operator-stop, or repository-control. Empty, malformed, unknown-version, unknown-origin, and otherwise unrecognized legacy content conservatively means operator-pause.

An explicit per-run Pause or Stop replaces repository control, and repository control never overwrites an operator source. When enabled and unhalted is observed, a live manager retracts only its exact owned repository-control path. An already-absent exact path counts as success; a real removal error retains ownership and pending state so a later observation retries. A process-local retraction tombstone keeps a child that read just before removal classified as stopped rather than operator-paused until it exits or a later explicit operator gesture replaces the tombstone.

A supervisor removes only signal files it successfully created for its own runs. It may reclaim another generation’s regular signal file only when the PID encoded in the complete name is proven dead. Alive or indeterminate owners are retained, as are malformed names, directories, symlinks, unreadable entries, and raced operations. Timestamps and elapsed time never authorize deletion. Exact UUID-scoped removal means a cleanup already in progress cannot target a newly created generation after PID reuse, and shutting down one host leaves another live host’s signals intact.

This layout records enough ownership for conservative cleanup; it is not a cross-host run-discovery or addressing protocol. Background output spools remain ephemeral in operating-system temporary storage and keep their separate process-liveness-gated reclamation.

Older Shipper versions could create .shipper/prompts/<agent>/ files. Current Shipper does not read .shipper/prompts/; prompt-driven CLI invocations warn once when regular files are present there, those files have no effect, and they may be removed.

Bundled prompt templates live in the Shipper package source under packages/core/, not in repository-local .shipper/ files.

Shipper does not add prompts/ to .shipper/.gitignore. If a repository still tracks legacy prompt files, remove them through a normal cleanup commit.

Files under .shipper/tmp/ (for example consumed question batches from headless grooming) may appear inside ephemeral stage worktrees. They are written per worktree by Shipper worktree plumbing, are not shared repository configuration, and should not be committed.

Sandboxed Codex directories live under the host /tmp tree, not under .shipper/tmp/. Shipper deletes current and abandoned sandboxed Codex directories only when exact ownership is proven. Matching path, owner, device, and inode is insufficient because a successor directory may reuse them. For abandoned reclamation, both the Shipper process and Codex child must also be proven dead. When exact ownership cannot be proven, Shipper preserves the candidate, warns with its specific location, and continues the requested stage with fresh isolated storage. Preparation failure that makes isolated storage unsafe prevents launch with a standalone temporary-storage error. Cleanup failures warn with the affected location and do not change the stage result or fall back to shared or uncertain storage. Shipper does not add --add-dir /tmp or promise exact-directory confinement; Codex may independently permit other system temporary directories.

Do not treat these files as part of the repository-local .shipper/ contract. They stay out of version control because they are worktree artifacts, not because .shipper/.gitignore names each file.

The repository’s top-level .gitignore should not ignore .shipper/ wholesale. Committed entries such as .shipper/settings.json, .shipper/.gitignore, .shipper/scripts/install-deps.sh, .shipper/input/.gitkeep, and .shipper/output/.gitkeep must stay trackable. Keep runtime artifact rules in .shipper/.gitignore instead.