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.
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, sendSIGTERMonly 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.
Lock-file existence is not treated as ownership. The tool independently:
- probes the actual OS lock with a nonblocking exclusive
flockattempt; - uses
lsofonly to correlate open file descriptors with a PID; - samples PID start time, command, PPID, TTY, cwd, lock inode, and rollout size/mtime across a stability window;
- requires exactly one same-user Codex owner holding exactly one thread lock;
- requires the rollout's last record to be
event_msg/task_complete; - refuses shared
app-server, Remote Control, and daemon owners; - 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;
- 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.
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.
- macOS or Linux
- Node.js 22.13+ (22.x) or Node.js 24.x
lsofavailable 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.
--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 lock2: unlock refused because the evidence was not safe3: termination/post-unlock verification, a missing or non-directory Codex home, or an unexpected command failure64: 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.
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.
codex-unlock check-update
npm install --global codex-unlock@latestNormal 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.
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 --helpnpm 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.