Skip to content

File paths: refuse a leading env reference, name the place it means - #391

Merged
rromenskyi merged 4 commits into
mainfrom
file-path-env-refs
Oct 20, 2026
Merged

rromenskyi merged 4 commits into
mainfrom
file-path-env-refs

Conversation

@rromenskyi

@rromenskyi rromenskyi commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #389.

The path never passes through a shell on its way to a file tool. So $env:USERPROFILE\.gitconfig was joined onto the workspace as a literal $env:USERPROFILE directory, and the read failed on a path the model never wrote. Before that, the risk check had scored, and the user had approved, the home directory the model meant.

The fix is to refuse such a path, not to expand it. The first version of this PR expanded the reference. Review showed that this was a way to read a secret. Reading $OPENAI_API_KEY resolved to <workspace>/sk-…, and the "no such file" error handed the key to the model with no prompt. Expanding also let a call open something other than what the approval prompt, the risk check and git had all seen.

policy.Engine.Resolve now refuses a path that starts with $NAME, ${NAME}, $env:NAME or ${env:NAME} (PowerShell's env: in any case), or %NAME%. The error reads: path "…" starts with $env:USERPROFILE, but file paths are not shell-expanded — pass the full path ($env:USERPROFILE is C:\Users\…).

  • The value is named only for a place: HOME, USERPROFILE, APPDATA, LOCALAPPDATA, TEMP, TMP, TMPDIR, PROGRAMDATA, PROGRAMFILES, SYSTEMROOT, WINDIR and XDG_*. Even then it is named only when the value is an absolute path. That way the model can retry with the full path at once. Any other variable's value never appears.
  • A reference counts only when a separator or the end of the path follows it. Foo$Bar.class, $Recycle.Bin and $HOME.bak stay file names.
  • ~\ on Windows now expands like ~/ does.

The refusal happens in Resolve, before any prompt, and it covers file, run cwd, git paths and /cd.

Tests: TestResolveRefusesALeadingEnvReference checks every spelling, that a secret's value never appears, and that the non-reference names still work. A sabotage pass made 7 mutations: no refusal, the hint leaking any variable, no separator check, case-sensitive env:, no braced env form, no hint, no cmd form. Every one failed the test.

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

Nothing on the way to a file tool is a shell, so "$env:USERPROFILE\.gitconfig"
was joined onto the workspace as a literal "$env:USERPROFILE" directory:
the read failed on a path the model never wrote, after the risk check had
scored (and the user approved) the home directory it meant.

Resolve now expands a leading $env:NAME, ${NAME}, $NAME or %NAME% to its
value, and ~\ on Windows like ~/. Only a variable that is set, and only
when a separator or the end follows — "Foo$Bar.class" and "$Recycle.Bin"
are file names. The jail and the deny floor run on the expanded path.

Fixes #389.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found expanding one was a way to read a secret: "$OPENAI_API_KEY"
as a read path resolved to <workspace>/sk-…, and the "no such file" error
handed the key to the model, with no prompt. It also let a call open
something other than what the approval prompt, the risk check and git had
seen.

Resolve now refuses such a path with "file paths are not shell-expanded —
pass the full path", naming the value only for a place (HOME, USERPROFILE,
APPDATA, TEMP, …) so the model can retry at once. Any other variable's
value never appears. PowerShell's env: matches in any case, and
${env:NAME} counts too. ~\ on Windows still expands, like ~/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rromenskyi rromenskyi changed the title File paths: a leading env reference resolves where it points File paths: refuse a leading env reference, name the place it means Oct 20, 2026
rromenskyi and others added 2 commits October 19, 2026 18:42
- A name's case is folded only on Windows: on Unix "home" is its own
  variable, and its value was named as if it were HOME's.
- A backslash after the reference counts as a separator on every system:
  models write "$env:USERPROFILE\.gitconfig" on macOS too.
- ${env:ProgramFiles(x86)} matches, like %ProgramFiles(x86)%.
- git passes the refusal on, hint included, instead of calling the path
  outside the workspace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.

File tool: shell env-var paths ($env:USERPROFILE\…, %USERPROFILE%, $HOME) are joined onto the workspace instead of expanded/rejected

1 participant