When PTY_ROOT and PTY_SESSION_DIR are both set and disagree, pty warns on stderr and exits 0. The warning is good — it names both values, says which one won, and tells you the fix. It is not enough, because a caller that discards stderr gets silent, complete loss of isolation, and the operations reachable from that state are destructive.
Measured on 0.12.x
$ A=$(mktemp -d); B=$(mktemp -d)
# both set, DISAGREEING
$ env PTY_ROOT=$A PTY_SESSION_DIR=$B pty list
pty: both PTY_ROOT and PTY_SESSION_DIR are set — using PTY_ROOT ($A);
PTY_SESSION_DIR ($B) is ignored (deprecated). For isolation, set PTY_ROOT.
No active sessions.
$ echo $?
0
# PTY_ROOT alone — silent, exit 0
# PTY_SESSION_DIR alone — deprecation notice, honoured, exit 0
The warning does not compare the values
$ env PTY_ROOT=$A PTY_SESSION_DIR=$A pty list # identical values
pty: both PTY_ROOT and PTY_SESSION_DIR are set — using PTY_ROOT ($A);
PTY_SESSION_DIR ($A) is ignored (deprecated). For isolation, set PTY_ROOT.
It fires on both being set, not on them disagreeing. Worth stating because it changes the shape of the fix: this is not "turn the existing warning into an error", it is add a comparison that does not exist today — error when the two disagree, and ideally stay quiet when they agree, since that case is unambiguous and currently produces a warning about an override that changes nothing.
Why exit status specifically
The realistic caller is a test fixture or script that builds an environment as
{ ...process.env, PTY_SESSION_DIR: tmpdir }
believing it has isolated itself. If the surrounding environment already exports PTY_ROOT — which any supervisor that manages long-lived sessions will do for every process it starts — then PTY_ROOT wins and the "isolated" handle is the real registry. The fixture then does what an isolated fixture reasonably does in teardown: pty list, pty kill each returned ref, pty gc.
Every one of those calls is individually legitimate, so there is nothing at the kill/gc layer to refuse. And the warning that would have caught it is on stderr, which such callers routinely discard — test runners buffer or swallow it, and stdio: 'ignore' drops it outright. An exit code cannot be swallowed.
The failure mode is quiet in an unusually bad way: the run passes. Isolation was arranged, never asserted, so the fixture cannot tell the difference between "isolated" and "pointed at everything".
Why CI cannot catch this class
In CI, PTY_ROOT is typically unset, so the alias is honoured, the isolation genuinely works, and the tests really are isolated. The defect exists only where the tool is actually used — on a machine with a live registry. A green suite is not evidence here, which is part of why a stderr-only signal has not been sufficient in practice.
Scope
- Stateless check at startup, no change to
kill/gc/run semantics — it cannot refuse a legitimate destructive operation, only an ambiguous configuration.
- Matching values stay legal.
PTY_SESSION_DIR alone stays legal and keeps its deprecation notice.
- Presumably
PTY_ROOT_LEGACY_SILENT should not suppress a hard failure, only the warning — otherwise the escape hatch reintroduces the hole.
Happy to send a PR if the shape is agreeable. Site is getSessionDir() in src/sessions.ts, where the existing warning is emitted.
Posted on behalf of @schickling
| field |
value |
agent_name |
unknown |
agent_tool |
Claude Code |
agent_tool_version |
2.1.220 |
agent_runtime |
Claude Code 2.1.220 |
agent_session_lookup |
unavailable |
tooling_profile |
dotfiles@unknown-dirty |
When
PTY_ROOTandPTY_SESSION_DIRare both set and disagree,ptywarns on stderr and exits0. The warning is good — it names both values, says which one won, and tells you the fix. It is not enough, because a caller that discards stderr gets silent, complete loss of isolation, and the operations reachable from that state are destructive.Measured on 0.12.x
The warning does not compare the values
It fires on both being set, not on them disagreeing. Worth stating because it changes the shape of the fix: this is not "turn the existing warning into an error", it is add a comparison that does not exist today — error when the two disagree, and ideally stay quiet when they agree, since that case is unambiguous and currently produces a warning about an override that changes nothing.
Why exit status specifically
The realistic caller is a test fixture or script that builds an environment as
believing it has isolated itself. If the surrounding environment already exports
PTY_ROOT— which any supervisor that manages long-lived sessions will do for every process it starts — thenPTY_ROOTwins and the "isolated" handle is the real registry. The fixture then does what an isolated fixture reasonably does in teardown:pty list,pty killeach returned ref,pty gc.Every one of those calls is individually legitimate, so there is nothing at the
kill/gclayer to refuse. And the warning that would have caught it is on stderr, which such callers routinely discard — test runners buffer or swallow it, andstdio: 'ignore'drops it outright. An exit code cannot be swallowed.The failure mode is quiet in an unusually bad way: the run passes. Isolation was arranged, never asserted, so the fixture cannot tell the difference between "isolated" and "pointed at everything".
Why CI cannot catch this class
In CI,
PTY_ROOTis typically unset, so the alias is honoured, the isolation genuinely works, and the tests really are isolated. The defect exists only where the tool is actually used — on a machine with a live registry. A green suite is not evidence here, which is part of why a stderr-only signal has not been sufficient in practice.Scope
kill/gc/runsemantics — it cannot refuse a legitimate destructive operation, only an ambiguous configuration.PTY_SESSION_DIRalone stays legal and keeps its deprecation notice.PTY_ROOT_LEGACY_SILENTshould not suppress a hard failure, only the warning — otherwise the escape hatch reintroduces the hole.Happy to send a PR if the shape is agreeable. Site is
getSessionDir()insrc/sessions.ts, where the existing warning is emitted.Posted on behalf of @schickling
agent_nameagent_toolagent_tool_versionagent_runtimeagent_session_lookuptooling_profile