Skip to content

Latest commit

 

History

History
262 lines (216 loc) · 13.3 KB

cli-reference.md

File metadata and controls

262 lines (216 loc) · 13.3 KB

CLI reference

Process discovery treats explicit lsof diagnostic output as incomplete evidence even when the command exits successfully, and recovery refuses. An empty diagnostic stream does not guarantee that every process was visible; lsof lists openers and its correlation is independent of the actual native lock probe. The repository's holder-attribution research records platform visibility assumptions and proposed stronger evidence.

codex-unlock diagnoses Codex native thread writer locks. Its supported automation interface is the CLI and JSON v1, with type-only TypeScript models at codex-unlock/types. There is no executable JavaScript library API. See the JSON contract for schema, types, and compatibility.

Commands

codex-unlock list [options]
codex-unlock inspect <thread-id> [options]
codex-unlock unlock <thread-id> [options]
codex-unlock check-update [options]
  • list: inspect threads with native lock files in the selected Codex home; it is not a list of every saved or resumable conversation.
  • inspect: collect lock, process, and transcript evidence for one thread.
  • unlock: revalidate a completed idle owner, send SIGTERM only if safe, then verify process exit, actual lock release, and transcript invariance.
  • check-update: explicitly check npm for a newer stable version; never install it.

list and inspect leave files unchanged but briefly take Codex's native coordination lock for each probe. Use --json on any command for one versioned JSON value. A live_owner classification is liveness evidence, not permission to unlock.

list inspects at most four threads concurrently and preserves sorted thread order. Each thread still gets independent before/after observations; no transcript discovery or safety evidence is shared between inspections. If an inspection fails, pending threads are not started and active inspections finish before the command returns its existing error result.

Safety model

Lock-file existence is not treated as ownership. The tool independently:

  1. probes the actual OS lock with a nonblocking exclusive flock attempt;
  2. uses lsof only to correlate open file descriptors with a PID;
  3. samples PID start time, command, PPID, TTY, cwd, lock inode, and rollout size/mtime across a stability window;
  4. requires exactly one same-user Codex owner holding exactly one thread lock;
  5. requires the rollout's last record to be event_msg/task_complete;
  6. refuses shared app-server, Remote Control, and daemon owners;
  7. repeats the complete safety inspection immediately before signaling, including process identity, every open native thread lock across Codex homes, lock inode/ownership, and the terminal transcript record;
  8. waits for process exit and actual lock release, then verifies that the transcript hash did not change.

Before a safe candidate can reach final revalidation and signaling, unlock acquires a private same-user advisory operation lease keyed by the native lock directory's device/inode and the thread UUID. A concurrent unlock is refused; this coordination file is outside native lock paths, and its existence is never lock evidence. Unsafe and already-unlocked cases do not create it.

The lease file lives in a codex-unlock/ directory beside the canonical (realpath) thread-writer-locks directory, which is normally $CODEX_HOME/codex-unlock/. Its location does not depend on TMPDIR, XDG_RUNTIME_DIR, or the spelling of --codex-home, so symlinked, case-variant, and differently configured shells coordinate on the same lease. The parent directory must be owned by the current user, the lease directory must be a same-user non-symlink directory with mode 0700, and the lease file must be a same-user regular single-link file with mode 0600; otherwise unlock refuses with unlock_coordination_failed:<reason>.

unlock never deletes native lock files, never sends SIGKILL, and has no force flag. Missing, ambiguous, or changing evidence fails closed. Persistent helper children are reported as warnings because only the lock-owning PID receives SIGTERM. Every ps and lsof subprocess has a deadline and bounded output; timeout, overflow, spawn, and parsing failures remain unknown evidence. --timeout-ms controls only the post-SIGTERM wait.

There is an unavoidable interval between final validation and the signal system call. Eliminating that last race requires an owner-cooperative upstream protocol. The handoff proposal is not an implemented command or fallback: shared or unsupported owners remain refused, and a failed handoff must never automatically chain to process termination.

Stale residue—an existing file with no actual OS lock—is reported but left in place. Codex handles stale lock-file cleanup during its coordinated startup. See the safety race matrix for regression coverage.

Native coordination during probes

Each probe of an existing thread lock first takes the existing thread-writer-locks/.coordination.lock with nonblocking exclusive flock. Writers following Codex's coordination protocol then wait for the probe instead of seeing a probe-induced WouldBlock on their thread try_lock.

Inspection retries a busy coordinator for up to 100 ms per probe, on a monotonic clock, with waits outside the guard. Inspection samples twice per thread; busy homes can therefore add up to roughly 200 ms of acquisition waits per inspected thread. Post-signal polling also retries within the remaining termination budget. The final pre-signal probe tries only once: contention refuses with native_coordination_busy_before_signal, keeping the last identity check and signal contiguous in the JavaScript turn.

If a thread lock exists but the coordinator is absent, unsafe, changed, or unavailable, its lock state is unknown and unlock refuses. The tool does not create a coordinator or fall back to probing without it. A verified absent thread lock still has the usual absent result. Copied or older homes lacking the coordinator can consequently change from live_owner or stale_residue to unknown.

The coordinator must be a current-user regular non-symlink file with one link; its path and descriptor identities and parent directory are checked around each probe. Normal 0644 and group-writable modes are allowed. Files are never rewritten, removed, or repaired. Contents, ownership, permissions, and mtime are unchanged; reading can affect atime.

The guard covers one synchronous probe, not a transcript scan, subprocess, signal, or exit wait. Pausing the diagnostic with SIGSTOP, a debugger, or a stalled process while it holds the guard can delay all coordinated writers in that home until the process continues or exits. The retry budget limits acquisition waits, not time spent paused while holding the lock. Custom writers that bypass native coordination are outside the interference guarantee.

Requirements

  • macOS or Linux
  • Node.js 22.13+ (22.x) or Node.js 24.x
  • lsof available on the host

The actual lock probe uses fs-ext-extra-prebuilt, which provides prebuilt native binaries for common macOS and Linux architectures. Node.js 26 is not supported because that dependency does not provide a compatible prebuilt binary. The platform evidence lists verified OS, architecture, and runtime combinations; unlisted combinations are unverified rather than implicitly supported.

Options and exit codes

--json                   Emit machine-readable JSON
--codex-home <path>       Defaults to nonempty CODEX_HOME or ~/.codex
--stability-ms <ms>       250..30000 (default: 1000)
--timeout-ms <ms>         100..60000 (default: 5000)
--no-update-notice       Disable cached notices and automatic refresh
-h, --help               Show help
-v, --version            Show version
  • 0: success, including an already-unlocked or absent lock
  • 2: unlock refused because the evidence was not safe
  • 3: termination/post-unlock verification, a missing or non-directory Codex home, or an unexpected command failure
  • 64: invalid command-line usage

Usage is validated before any inspection. A malformed thread id, an option value that begins with - (for example --codex-home --json), or --help / --version combined with --json is a usage error with exit 64. -h, --help, -v, and --version are honoured only as their own arguments, never as another option's value. Help output to a closed pipe ends quietly.

Selecting the Codex home

An explicit --codex-home <path> takes precedence over CODEX_HOME, including an empty or unavailable environment path. Without that option, home selection is:

CODEX_HOME Selected home
Unset The current user's home directory plus .codex
Exactly empty (CODEX_HOME='') The same default as unset
Nonempty relative path Resolved against the command's current working directory
Absolute path That absolute path

Only the exactly empty string is ignored; whitespace is a nonempty path and is not trimmed. Relative --codex-home paths also resolve against the current working directory. An empty CLI path (--codex-home '') is a usage error with exit 64, rather than a request for the default home. Help and version remain usable regardless of whether the selected home exists.

This aligns empty environment values with the verified Codex Rust releases 0.159.2 and 0.160.0. Their pinned implementations filter out an empty CODEX_HOME before selecting the default and validate and canonicalize nonempty paths: 0.159.2 source and 0.160.0 source. That source establishes the four cases above; it does not verify other Codex versions. codex-unlock retains its existing absolute path resolution rather than adding upstream's canonicalization to home selection.

Compatibility change: in codex-unlock v0.4.3, an empty CODEX_HOME selected the current working directory. It now selects the usual .codex home, matching the verified upstream behavior. To deliberately inspect the current directory, use --codex-home ..

The selected Codex home must be an existing directory. A missing home (for example a --codex-home typo, a different CODEX_HOME, or an unmounted volume) is a command failure with exit 3, never an absent lock. A home that exists but has not yet created thread-writer-locks is a normal confirmed absence: list is empty and inspect reports absent. A dangling symlink or non-directory at thread-writer-locks is not evidence of absence: inspect reports unknown, unlock refuses with exit 2, and list fails with exit 3.

Human-readable output renders externally derived text, such as owner arguments, cwd, TTY, lock and transcript paths, transcript event types, reasons, and error text, with visible escapes. C0 controls, DEL, C1 controls, and Unicode bidirectional or line-separator controls appear as \n, \r, \t, \xHH, or \uHHHH, so prompt text, directory names, or transcript content cannot move the cursor, change the terminal, or forge result lines. JSON output keeps the original values and relies on standard JSON escaping.

With --json, stdout is one JSON value and human diagnostics are not mixed into stderr. Usage and command failures retain the top-level error string and add stable errorCode, exitCode, and schemaVersion fields.

Updates

codex-unlock check-update
npm install --global codex-unlock@latest

Normal commands never wait for the network. An interactive command may show a fresh cached advisory on stderr after its primary result, then start a detached best-effort cache refresh when the cache is missing or expired. JSON output instead uses an optional versioned clientUpdate field and never mixes advisory text into stderr. CI and non-TTY human commands do not show or refresh notices. The safety-critical unlock path never starts a registry request or update refresher during inspection, revalidation, signaling, or post-signal verification. Any automatic refresh starts only after the command has completed and printed its primary result.

check-update (also available with --json) is an explicit foreground registry check. It does not install or automatically update the package. Set CODEX_UNLOCK_NO_UPDATE_NOTICE=1 or pass --no-update-notice to disable cache reads, notices, and automatic refreshes; these do not disable an explicit check-update. See the update security contract for permissions, offline behavior, bounds, and failure isolation.

Source installation

The supported published installation is npm install --global codex-unlock. For source development:

git clone https://github.com/abruption/codex-unlock.git
cd codex-unlock
npm ci
npm link
codex-unlock --help

npm ci runs the prepare build. Direct global Git dependency installation (npm install --global git+https://...) is not supported; some npm versions omit build-time dependencies when preparing Git packages. Use the cloned source or npm registry installation instead.

See Contributing for development checks and v0.2 migration for the historical 0.1.1 integration boundary.