Skip to content

fix(file-tools): reject sentinel TERMINAL_CWD; anchor worktree edits before live cwd exists#41861

Merged
teknium1 merged 1 commit into
mainfrom
fix/terminal-cwd-sentinel-anchor
Jun 8, 2026
Merged

fix(file-tools): reject sentinel TERMINAL_CWD; anchor worktree edits before live cwd exists#41861
teknium1 merged 1 commit into
mainfrom
fix/terminal-cwd-sentinel-anchor

Conversation

@teknium1

@teknium1 teknium1 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

A worktree dev session can no longer silently misroute a relative write_file/patch into the main checkout — sentinel/relative TERMINAL_CWD values are rejected as anchors, and the worktree is used (and divergence is warned about) from the very first write, before any terminal command has run.

This completes the fix from #35399. That PR made the misroute visible (it added resolved_path to every write), but it did not prevent it: its divergence warning only fired once a terminal command had populated the live-cwd registry. A fresh worktree session — registry still empty, TERMINAL_CWD left at the stale sentinel "." — got neither a worktree anchor nor a warning, so a relative edit landed in main, self-verified, and reported success against the wrong file. (Hit live this session.)

Root cause

_resolve_base_dir() treated a relative TERMINAL_CWD (commonly ".") as a literal base and joined it onto the agent's process cwd — which, in a -w session, is the main repo. With no live terminal cwd yet recorded, the relative edit resolved into main. _path_resolution_warning() returned early whenever the live cwd was unknown, so nothing flagged it.

Changes (tools/file_tools.py)

  • Sentinel handling: '', ., ./, auto, cwd (and any relative value) are treated as unset, never as a literal anchor. The gateway already sanitizes this set at import time (gateway/run.py); the file-tool layer now matches, so CLI sessions get the same protection.
  • New _authoritative_workspace_root(): prefers the live terminal cwd, else a sentinel-free absolute TERMINAL_CWD (the worktree path cli.py/main.py set for -w). Both _resolve_base_dir() and _path_resolution_warning() use it — so a worktree session resolves into, and warns about escaping, the worktree from the first write.
  • _resolve_base_dir() falls through to the process cwd only as a deterministic last resort.

Validation

Scenario (registry empty, process cwd = main) Before After
TERMINAL_CWD='.' + relative edit lands in main (silent) falls to process cwd; sentinel never used as anchor
TERMINAL_CWD=<abs worktree> + relative edit lands in main (live cwd unknown) lands in worktree
relative edit escaping the worktree no warning divergence _warning fires on first write
live terminal cwd present used used (still authoritative)
  • 11 new/parametrized unit tests (sentinel handling, empty-registry anchoring, early divergence warning, live-cwd precedence) in tests/tools/test_file_tools_cwd_resolution.py.
  • 32/32 pass under scripts/run_tests.sh (per-file process isolation = CI condition).
  • Live E2E: relative write_file in an empty-registry worktree session lands in the worktree; main untouched.

Note: tests/tools/test_terminal_task_cwd.py has 3 failures that are pre-existing on main (confirmed by stashing this change) and unrelated to this PR — they assert a fake_env.cwd mutation in terminal_tool.py, not file-tool resolution. Flagging for a separate fix.

Infographic

terminal-cwd-anchoring

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: fix/terminal-cwd-sentinel-anchor vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10111 on HEAD, 10111 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5238 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tools Tool registry, model_tools, toolsets tool/file File tools (read, write, patch, search) labels Jun 8, 2026
@teknium1 teknium1 force-pushed the fix/terminal-cwd-sentinel-anchor branch from 220b625 to 55a9245 Compare June 8, 2026 06:42
…before live cwd exists

Completes the worktree-misroute fix from #35399, which made misroutes
visible (resolved_path) but did not prevent them: its divergence warning
only fired once a terminal command had populated the live cwd registry.
A fresh worktree session (registry still empty) with a stale TERMINAL_CWD='.'
got neither a worktree anchor nor a warning, so a relative write_file/patch
silently landed in the MAIN checkout.

Two changes in tools/file_tools.py:
- Treat sentinel TERMINAL_CWD values ('', '.', './', 'auto', 'cwd') and any
  relative value as UNSET rather than a literal anchor. Previously '.' was
  joined onto the process cwd, silently routing edits to wherever the process
  happened to be (the main repo, in a worktree session). The gateway already
  sanitizes the same set at import time; the file-tool layer now matches.
- New _authoritative_workspace_root(): prefers the live terminal cwd, else a
  sentinel-free absolute TERMINAL_CWD (the worktree path cli.py/main.py set
  for -w). _resolve_base_dir() and _path_resolution_warning() both use it, so
  a worktree session resolves into — and warns about escaping — the worktree
  from the very first write, before any cd has run.

Validation: 11 new/parametrized tests (sentinel handling, empty-registry
anchoring, early divergence warning, live-cwd precedence). 32/32 pass under
scripts/run_tests.sh. Live E2E: relative write in an empty-registry worktree
session lands in the worktree, main untouched.
@teknium1 teknium1 force-pushed the fix/terminal-cwd-sentinel-anchor branch from 55a9245 to 2106717 Compare June 8, 2026 06:45
@teknium1 teknium1 merged commit e45b745 into main Jun 8, 2026
39 of 46 checks passed
@teknium1 teknium1 deleted the fix/terminal-cwd-sentinel-anchor branch June 8, 2026 06:58
a249169329-cpu pushed a commit to a249169329-cpu/hermes-agent that referenced this pull request Jun 8, 2026
…before live cwd exists (NousResearch#41861)

Completes the worktree-misroute fix from NousResearch#35399, which made misroutes
visible (resolved_path) but did not prevent them: its divergence warning
only fired once a terminal command had populated the live cwd registry.
A fresh worktree session (registry still empty) with a stale TERMINAL_CWD='.'
got neither a worktree anchor nor a warning, so a relative write_file/patch
silently landed in the MAIN checkout.

Two changes in tools/file_tools.py:
- Treat sentinel TERMINAL_CWD values ('', '.', './', 'auto', 'cwd') and any
  relative value as UNSET rather than a literal anchor. Previously '.' was
  joined onto the process cwd, silently routing edits to wherever the process
  happened to be (the main repo, in a worktree session). The gateway already
  sanitizes the same set at import time; the file-tool layer now matches.
- New _authoritative_workspace_root(): prefers the live terminal cwd, else a
  sentinel-free absolute TERMINAL_CWD (the worktree path cli.py/main.py set
  for -w). _resolve_base_dir() and _path_resolution_warning() both use it, so
  a worktree session resolves into — and warns about escaping — the worktree
  from the very first write, before any cd has run.

Validation: 11 new/parametrized tests (sentinel handling, empty-registry
anchoring, early divergence warning, live-cwd precedence). 32/32 pass under
scripts/run_tests.sh. Live E2E: relative write in an empty-registry worktree
session lands in the worktree, main untouched.
changman pushed a commit to changman/hermes-agent that referenced this pull request Jun 10, 2026
…before live cwd exists (NousResearch#41861)

Completes the worktree-misroute fix from NousResearch#35399, which made misroutes
visible (resolved_path) but did not prevent them: its divergence warning
only fired once a terminal command had populated the live cwd registry.
A fresh worktree session (registry still empty) with a stale TERMINAL_CWD='.'
got neither a worktree anchor nor a warning, so a relative write_file/patch
silently landed in the MAIN checkout.

Two changes in tools/file_tools.py:
- Treat sentinel TERMINAL_CWD values ('', '.', './', 'auto', 'cwd') and any
  relative value as UNSET rather than a literal anchor. Previously '.' was
  joined onto the process cwd, silently routing edits to wherever the process
  happened to be (the main repo, in a worktree session). The gateway already
  sanitizes the same set at import time; the file-tool layer now matches.
- New _authoritative_workspace_root(): prefers the live terminal cwd, else a
  sentinel-free absolute TERMINAL_CWD (the worktree path cli.py/main.py set
  for -w). _resolve_base_dir() and _path_resolution_warning() both use it, so
  a worktree session resolves into — and warns about escaping — the worktree
  from the very first write, before any cd has run.

Validation: 11 new/parametrized tests (sentinel handling, empty-registry
anchoring, early divergence warning, live-cwd precedence). 32/32 pass under
scripts/run_tests.sh. Live E2E: relative write in an empty-registry worktree
session lands in the worktree, main untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists tool/file File tools (read, write, patch, search) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants