Skip to content

fix: tolerate login-shell process names in the ancestry walks - #3

Merged
sanis merged 3 commits into
mainfrom
fm/fm-basename-login-shell
Jul 29, 2026
Merged

sanis merged 3 commits into
mainfrom
fm/fm-basename-login-shell

Conversation

@sanis

@sanis sanis commented Jul 29, 2026 •

Copy link
Copy Markdown
Owner

Problem

On macOS a login shell reports itself through ps -o comm= with a leading dash:

$ basename "-zsh"
basename: illegal option -- z
usage: basename string [suffix]

The process-ancestry walks pass that name straight to basename, which parses
-zsh as the option flags -z -s -h and errors. Every walk that reaches a login
shell printed basename: illegal option -- z to stderr, on effectively every
turn-end check. The walk continued, so this was noise rather than corruption -
but it polluted output where genuine warnings must stand out, and an argument
silently reinterpreted as flags is a latent correctness trap.

Fix

Every ps-derived basename call now strips the directory with shell parameter
expansion instead:

  • bin/fm-session-lock-lib.sh - fm_harness_ancestry_pid and fm_harness_pid_alive
  • bin/fm-harness.sh - detect_own

bin/ and bin/backends/ were swept for every ps consumer; those three were
the only ones passing a process name to basename. bin/fm-backend.sh and
bin/backends/tmux.sh already pattern-match on the full comm (tmux even strips
the leading dash itself). basename calls on ordinary file paths are untouched
and out of scope.

Why parameter expansion rather than basename --

Both are correct. ${comm##*/} was chosen because it avoids a subprocess on
every hop of a walk that runs up to sixteen times per check, and it matches the
expansion style already used in the backends.

Behaviour is otherwise preserved exactly. For every value ps -o comm= can
produce, ${comm##*/} and basename -- agree. These walks resolve session-lock
ownership and harness identity, so a behaviour change here would be a safety
change; there is none.

Tests

The regression case lives in tests/fm-secondmate-harness.test.sh, the existing
owner of process-identity coverage for both walks. It places a -zsh process
mid-ancestry with a real harness above it and asserts both correct resolution and
empty stderr, across detect_own, fm_harness_ancestry_pid, and
fm_harness_pid_alive.

  • Before the fix: fails with harness detection wrote to stderr: basename: illegal option -- z.
  • After the fix: passes.

Test hermeticity (follow-up commits)

Review surfaced that the pre-existing sibling test
test_pi_signed_detection_and_session_lock_identity in the same file depended on
ambient harness markers: with CLAUDECODE set it resolved claude instead of
pi, and with an ambient PI_CODING_AGENT, GROK_AGENT, or FM_PI_HARNESS its
assertions flipped to the branch they were written to rule out. It was passing
without validating what it claimed to.

Those invocations now scrub exactly the markers each assertion is proving the
absence of - matching the convention already used in
tests/fm-session-start.test.sh. No assertion, message, or structure changed.
The file now passes with CLAUDECODE set, so the marker was masking a hermeticity
problem, not a defect in harness detection.

Verification

  • bin/fm-lint.sh clean.
  • tests/fm-secondmate-harness.test.sh passes, including with CLAUDECODE=1 set.
  • fm-claude-stop-autoarm, fm-kimi-harness, fm-grok-harness, and
    fm-session-start all pass.

Summary by CodeRabbit

  • Bug Fixes

    • Improved harness and session-lock detection when running through login shells, including shells whose process names begin with a dash.
    • Reduced unnecessary background processing during process ancestry checks.
    • Prevented potential misidentification of harness processes in certain shell environments.
  • Tests

    • Added coverage for login-shell ancestry detection and constrained environment scenarios.

sanis added 3 commits July 29, 2026 10:51
On macOS a login shell reports itself through `ps -o comm=` as "-zsh".
Passing that to basename makes it parse "-z -s -h" as option flags and
fail loudly on stderr, so every ancestry walk that reached a login shell
printed "basename: illegal option -- z" - noise where genuine warnings
must stand out, and an argument silently reinterpreted as flags.

Strip the directory with parameter expansion instead of basename in
fm_harness_ancestry_pid, fm_harness_pid_alive, and fm-harness.sh's
detect_own. Parameter expansion also avoids a subprocess on every hop of
a walk that runs up to sixteen times per check. Behavior is otherwise
unchanged: for every value `ps -o comm=` produces, "${comm##*/}" and
`basename --` agree.

Adds a regression case to tests/fm-secondmate-harness.test.sh, the
existing owner of both ancestry walks' process-identity coverage.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f70107b-b927-492d-9a45-0261666facb4

📥 Commits

Reviewing files that changed from the base of the PR and between 88072e9 and f2544ec.

📒 Files selected for processing (3)
  • bin/fm-harness.sh
  • bin/fm-session-lock-lib.sh
  • tests/fm-secondmate-harness.test.sh

📝 Walkthrough

Walkthrough

Updated harness and session-lock ancestry detection to extract executable names with Bash parameter expansion, plus tests for macOS-style login-shell names and controlled environment markers.

Changes

Harness ancestry detection

Layer / File(s) Summary
Normalize ancestry process names
bin/fm-harness.sh, bin/fm-session-lock-lib.sh
Harness detection and session-lock liveness checks now use ${comm##*/} instead of basename when matching process names.
Validate login-shell ancestry handling
tests/fm-secondmate-harness.test.sh
Tests unset environment markers for deterministic assertions and verify detection across a simulated -zsh ancestry process without stderr output.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant fm-harness.sh
  participant fm-session-lock-lib.sh
  participant FakePS
  Test->>FakePS: Report ancestry including -zsh
  fm-harness.sh->>FakePS: Read process names
  FakePS-->>fm-harness.sh: Return harness and login-shell names
  fm-harness.sh-->>Test: Resolve harness
  fm-session-lock-lib.sh->>FakePS: Read ancestry and liveness
  FakePS-->>fm-session-lock-lib.sh: Return process information
  fm-session-lock-lib.sh-->>Test: Resolve session-lock holder
Loading

Suggested reviewers: kunchenguid

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: handling login-shell process names during ancestry walks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fm/fm-basename-login-shell

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sanis
sanis merged commit cdcedcb into main Jul 29, 2026
1 check passed
sanis pushed a commit that referenced this pull request Sep 13, 2026
…kunchenguid#3578)

* fix(bin): let verified harness ancestry outrank retained markers (#3)

* fix(bin): let a structural harness ancestor outrank a retained marker

bin/fm-harness.sh treated a verified environment marker as unconditionally
authoritative, so a Codex session started from an environment that had retained
CLAUDECODE=1 detected as claude. Session start then emitted Claude's Stop-owned
supervision protocol to a Codex primary, and every turn end was blocked for
missing Claude recovery.

The defect is the precedence boundary, not any one harness. codex, opencode,
kimi, and muse publish no identity marker at all, so with markers winning
outright any retained CLAUDECODE renamed them; the Cursor-before-Claude ordering
was a point patch on the same class of problem, and the launch-time marker
clearing only ever covered sessions fm-spawn started.

Markers and ancestry are now separate evidence layers that detect_own arbitrates:

- no ancestry match, or no marker: the single available layer answers, unchanged;
- same harness family: the marker's finer verdict stands, so a launch-selected
  pi-signed is not flattened to pi by an ancestry walk that can only see the
  shared launcher name;
- different harness with a structural (command-name) ancestor: ancestry wins,
  because only ancestry proves who owns the process tree;
- different harness with only a bare-interpreter script-path match: the marker
  wins, since a harness-shaped path in some node process's arguments is weaker
  evidence than a harness publishing its own identity.

The correction is symmetric: a retained CURSOR_AGENT no longer renames a claude
worker nested under cursor either.

Adds fm-harness.sh ancestry [<pid>], ancestry evidence with no marker layer, so
a real harness process can be asked what the walk makes of it.

tests/fm-harness-precedence.test.sh is the portable regression, built from real
renamed processes with no harness installed. Every case drives the two layers
apart and asserts each alone as well as the combination, so no case can pass
vacuously; it also pins Codex's real two-process install topology, since the fix
depends on the native binary being what a tool subprocess meets first. The
opt-in drift guard gains the matching live half: each installed harness's real
running process must still be identified by the ancestry walk, and it fails
naming the harness and version when a release changes that name.

Documentation follows the corrected contract in the script header, the
harness-adapters detection section, the codex, opencode, kimi, and cursor
references, and a dated verification record.

* fix(tests): drop the unused argument pass-through in the shim-topology helper

bin/fm-lint.sh refused the branch: run_shim declared a `[ancestry]` argument and
forwarded "$@", but every call site that varies the environment or passes the
ancestry subcommand invokes the shim entry point directly, so the helper is only
ever called with no arguments (ShellCheck SC2120/SC2119).

Behavior is unchanged: with no arguments "$@" expanded to nothing.

* fix(bin): examine the top of the process chain instead of assuming init

harness_ancestry stopped as soon as the next pid was 1, on the assumption that
pid 1 is always init and can never be a harness.
Inside a PID namespace that assumption inverts: the harness itself is pid 1, so
the walk never examined the one process that proves who owns the tree, reported
no ancestry at all, and handed the verdict straight back to a retained marker.

A real Codex session under `codex sandbox`, holding CLAUDECODE=1 and
CLAUDE_CODE_ENTRYPOINT=cli, is exactly that shape: it resolved claude and
rendered Claude's Stop-owned supervision protocol even with the marker-vs-ancestry
precedence boundary in place.
The same probe now resolves codex and renders the Codex foreground checkpoint.

A host's real pid 1 (init, systemd, launchd) matches no harness name, so
examining it costs one ps call and can introduce no false positive; the walk
still stops once that top process has been read, and a non-numeric or zero ppid
still ends it.

tests/fm-harness-precedence.test.sh pins the namespace shape with a fake ps that
reports every process as bash with ppid 1 and pid 1 as the harness.
The case asserts the marker still answers alone when pid 1 is host-shaped, so it
cannot pass vacuously, and it fails against the previous stop condition.

* docs(verification): record the real-Codex retained-marker evidence

The existing record proved the precedence boundary with the portable regression
and recorded each installed harness's process name behind the ancestry walk, but
it had no evidence from a real Codex process actually holding a retained Claude
marker, which is the failure the boundary exists for.

Adds the dated before/after result from codex-cli 0.152.0 under `codex sandbox`,
with the exact command and the decisive verdict and rendered protocol on each
side, and records the second boundary that shape exposed: the walk must examine
the top of the process chain, because inside a PID namespace the harness is pid 1.
Refreshes the portable regression's observed output for the case it gained.

* no-mistakes(review): blind ancestry in marker-pinned harness tests

* no-mistakes(review): blind ancestry in the Pi guard-routing test

* no-mistakes(review): classify precedence suite, dedupe ps stub, soften claims

* no-mistakes(review): model the spawn-and-wait Codex shim topology

* no-mistakes(document): correct stale muse marker-clearing detection claims

* no-mistakes: apply CI fixes

* fix(bin): examine the top of the chain in the lock and nudge walks too

The pid-1 defect corrected in bin/fm-harness.sh survived unchanged in the two
other harness-ancestry walks, on the exact topology the branch verified against
a real Codex process.

bin/fm-session-lock-lib.sh's fm_harness_ancestry_pids stopped as soon as the next
pid was 1, so a firstmate whose harness is pid 1 of its own PID namespace could
not find that harness at all and did not recognize its own session lock.
bin/fm-sessionstart-nudge.sh carried the same stop plus a blanket rejection of a
lock pid of 1, so the same session was told to run session start again on every
turn.

Both walks now compare the top process before stopping, matching the shape used
in bin/fm-harness.sh.
For the lock walk this is safe because fm_harness_process_matches rejects a
host's real pid 1.
For the nudge, `kill -0` still gates the lock pid, and on a host an unprivileged
`kill -0 1` fails, so a lock file that wrongly names pid 1 leaves the hook silent
rather than acting on init.

Each walk gains one regression case. The lock case drives a deterministic process
table whose pid 1 is the harness and asserts a host-shaped pid 1 still finds
nothing, so it cannot pass vacuously. The nudge case needs a real PID namespace,
because the builtin `kill -0` gate cannot be reached through a fake ps, and it
first proves the same fixture nudges with no lock present; it skips explicitly
where unprivileged namespaces are unavailable.

* no-mistakes(review): assert comm-strength detection from subprocess vantage in drift guard

* fix(bin): verify the live harness guard at the strength the guarantee needs

The marker-versus-ancestry boundary this branch ships is a strength claim:
detect_own hands an args-strength verdict straight back to a retained foreign
marker, so a harness is only protected where the ancestry walk reaches it at
comm strength.

The installed-harness drift guard probed the pane process alone. Under an
interpreter shim the pane process IS the shim, whose own script path is args
strength, while the native binary that carries comm strength is its child. The
guard therefore observed args for Codex, passed, and would have kept passing if
a release stopped spawning that native child at all, while real sessions
silently regressed to the original bug.

fm-harness.sh gains `ancestry-subtree`, which asks the walk from the pane
process and every descendant of it, the vantage a tool subprocess actually
occupies. The guard now requires comm strength somewhere in that set and
requires every vantage to name the same harness.

This supersedes the preceding commit's in-guard leaf walk, which reached the
same vantage but left the logic inside the test file, where CI could not pin it
and nothing else could reuse it. A harness-dependent check needs both halves:
`tests/fm-harness-precedence.test.sh` now carries a portable case proving the
subtree probe reaches a strength the top-of-session probe cannot, mutation
checked twice, once against the pre-change script and once by disabling
descendant enumeration. The subtree walk also avoids depending on tty and
process-group semantics that differ between Linux and macOS.

Verified live: codex-cli 0.152.0 reports [args codex;comm codex] and Claude Code
2.1.257 reports [comm claude].

* no-mistakes(review): narrow drift guard to the upward vantage path

* no-mistakes(review): judge only comm-strength vantages in drift guard

* no-mistakes(document): drop duplicated rationale in detection precedence evidence

* no-mistakes(review): fix pid-1 nudge case vacuity and descent no-arg expansion

* no-mistakes(document): drop branch-relative phrasing in detection precedence evidence

* no-mistakes(review): guard remaining empty positional expansions in fm-harness

* no-mistakes(document): scope cursor marker-ordering claim to the marker layer

* no-mistakes(review): Prefer comm-strength leaves in equal-depth descent ties

* no-mistakes(document): Document comm-strength descent tie-break

---------

* no-mistakes(review): Blind ancestry in stale gemini/rovo marker-precedence tests

* no-mistakes(document): Add missing equal-depth-tie test line to precedence evidence transcript

* no-mistakes(review): Fix stale/vacuous agy precedence test, add agy to precedence suite and docs

* no-mistakes(document): Fix stale kimi.md marker doc missed by ancestry-precedence fix

---------

Co-authored-by: NewAiCoder <170579485+NewAiCoder@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant