Skip to content

Exit non-zero when PTY_ROOT and PTY_SESSION_DIR disagree: the warning is on stderr, and the callers that need it discard stderr #163

Description

@schickling-assistant

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions