.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.
Standard entries
Section titled “Standard entries”settings.json
Section titled “settings.json”- Purpose: Shared repository configuration for Shipper.
- Created or updated by:
shipper setup --repowrites committed intent rather than copying all built-in defaults. A fresh file contains the repositorycliVersionand aninstallCommandonly when setup discovered or explicitly confirmed one. Manage other shared settings—including a deliberate agent pin—explicitly withshipper settings set <key> <value> --repoorshipper settings unset <key> --repo. - Version control: Committed. See Settings for the setup contract, schema, scopes, and precedence.
settings.local.json
Section titled “settings.local.json”- 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.
hooks/
Section titled “hooks/”- Purpose: Executable hook scripts that run around Shipper stages and worktree lifecycle events.
- Created or updated by:
shipper setup --repocreates the directory on disk. Users add executable hook scripts. Bareshipper setup --assistdoes not create hooks by default; it may create.shipper/hooks/worktree-setuponly after explicit opt-in to a committed bootstrap-hook fallback when oneinstallCommandcannot 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-setupis a commit-trackable repository artifact when chosen. An empty directory is not represented in git. Hook names and semantics belong in Configure hooks.
scripts/
Section titled “scripts/”- Purpose: Committed scaffold-managed helper scripts used by Shipper runs.
- Created or updated by:
shipper setup --repocreates the directory and writes helper scripts, currently including.shipper/scripts/install-deps.sh. Every stage worktree uses Shipper’s current bundled copy ofinstall-deps.sh, so hand-editing the committed copy has no effect on dependency installation. SetinstallCommandto 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 --repocreates 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.
input/
Section titled “input/”- Purpose: Per-stage context files that Shipper provides to agents.
- Created or updated by:
shipper setup --repocreates the directory and writesinput/.gitkeep. Runtime code writes context files there during stage runs. - Version control:
input/.gitkeepis committed so the directory exists in fresh checkouts. All otherinput/*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.
output/
Section titled “output/”- Purpose: Stage protocol outputs written by agents for Shipper to consume.
- Created or updated by:
shipper setup --repocreates the directory and writesoutput/.gitkeep. Agents write result, comment, and payload artifacts there during stage runs. - Version control:
output/.gitkeepis committed so the directory exists in fresh checkouts. All otheroutput/*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.
README.md
Section titled “README.md”- Purpose: Short local orientation for someone opening
.shipper/in an initialized repository. - Created or updated by:
shipper setup --repowrites the file from Shipper’s README template. - Version control: Gitignored by
.shipper/.gitignore; not committed.
.gitignore
Section titled “.gitignore”- Purpose: Runtime ignore rules for generated
.shipper/files. - Created or updated by:
shipper setup --repowrites the file. - Version control: Committed.
The current runtime ignore contract is exactly:
tmp/settings.local.jsonREADME.mdinput/*!input/.gitkeepoutput/*!output/.gitkeepUser-home storage
Section titled “User-home storage”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.jsonstores user-global settings.~/.shipper/config/<storageKey>/settings.jsonstores 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 atgroom-question-sessions/s<localId>.json, survive CLI and MCP host restarts, and have no age-based expiry. Their version 1 record may containownerSurface: "desktop" | "mcp" | "terminal". Absence means the legacy session is unowned; readers never infer or backfill an owner. The same schema version may contain optionalprOverlapEvidence.activeplus a one-hopsupersedesreceipt. 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 onlyprOverlapEvidenceandupdatedAt.~/.shipper/sessions/<storageKey>/stores agent sessions and usage metadata. See Session metadata for what a session sidecar records.~/.shipper/auto-ship/<storageKey>/state.jsonstores the combined Auto-ship desired state and durable safety-stop latch/history for one canonical repository.~/.shipper/auto-ship/<storageKey>/dispatch-owner.jsonis the machine-global, per-repository live-dispatcher record within one Shipper home. Its siblingdispatch-owner.lockguards atomic mutation by Terminal auto-ship, Desktop, and the daemon.~/.shipper/run-history/<storageKey>/records.jsonstores 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.jsonstores 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.lockand the fallback address path’s siblingrecords.lockserialize mutations.~/.shipper/repos/stores managed repository clones.~/.shipper/repository-access/<sha256("named\0" + normalizedOwnerRepo)>/stores the repository-scoped cross-process managed-clone lease. Itsstate.lockserializes lease-record mutations;writer-intent.jsonexpresses writer preference, and token-named records underreaders/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 explicitshipper cache clearremoves the entire legacy root.~/.shipper/recovery/replies/preserves output-protocol replies that could not be delivered.~/.shipper/run/daemon.sockis the per-user daemon endpoint (one daemon per Shipper home);~/.shipper/run/daemon.owner.jsonis its fenced ownership record, and~/.shipper/run/daemon-status.jsonis advisory process and service state.~/.shipper/currentis the installer’s atomic symlink to the current immutable CLI generation under~/.shipper/cli/<version>-<generation>/shipper-cli;~/.shipper/current/bin/shipperis 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.
Machine-wide dependency and build caches
Section titled “Machine-wide dependency and build caches”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.
Daemon runtime
Section titled “Daemon runtime”~/.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.
Auto-ship repository control record
Section titled “Auto-ship repository control record”~/.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.
Auto-ship dispatch-ownership record
Section titled “Auto-ship dispatch-ownership record”~/.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.
Background-run history
Section titled “Background-run history”~/.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.
Session metadata
Section titled “Session metadata”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 asopusto 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
usagetotals 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.
Background pause signaling
Section titled “Background pause signaling”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.
Legacy leftovers
Section titled “Legacy leftovers”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.
Worktree-only artifacts
Section titled “Worktree-only artifacts”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.
Project-root .gitignore
Section titled “Project-root .gitignore”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.