Skip to content

feat(shell): sensitive-path pattern guard for run_command args (#259)#911

Open
KuaaMU wants to merge 2 commits into
esengine:mainfrom
KuaaMU:feat/sensitive-path-guard
Open

feat(shell): sensitive-path pattern guard for run_command args (#259)#911
KuaaMU wants to merge 2 commits into
esengine:mainfrom
KuaaMU:feat/sensitive-path-guard

Conversation

@KuaaMU
Copy link
Copy Markdown
Contributor

@KuaaMU KuaaMU commented May 15, 2026

What

Path-argument scanner on top of the existing allowlist gate. When a run_command or run_background call matches the allowlist but one of its argv tokens resolves into a sensitive prefix or matches a sensitive filename pattern, the call is demoted to the confirm gate — same code path as RISKY_ARGS.

Why

Read-only commands like cat, grep, find, head, tail are on the fast path with any path argument. The agent can cat ~/.ssh/id_rsa or grep -r AWS_SECRET /etc and pipe the contents straight into the LLM context — a real exfiltration vector even on a local CLI.

OWASP AI Agent Cheat Sheet § 8 (Data Protection / Path traversal) and § 1 frame this as the complement to per-tool least-privilege: not just which command, but against which resources.

Default blocklist

Path prefixes (tilde-relative, resolved at runtime):

  • ~/.ssh, ~/.aws, ~/.gnupg, ~/.kube
  • /etc/shadow, /etc/sudoers

Filename patterns (case-insensitive basename match):

  • *.env, *.env.*, *.key, *.pem
  • id_rsa*, id_ed25519*
  • *credentials*, *secret*

User configuration

# ~/.reasonix/config.yaml
sensitivePaths:
  prefixes:
    - /opt/secrets
    - ~/work/vault
  patterns:
    - "*.p12"
    - "*token*"

Changes

File What
src/tools/shell/parse.ts Default blocklist, hasSensitivePathArgs(), isAllowed()/isCommandAllowed() accept optional projectRoot + sensitivePathConfig
src/tools/shell.ts ShellToolsOptions.sensitivePaths, pass rootDir + config through to dispatch
src/config.ts ReasonixConfig.sensitivePaths
tests/shell-tools.test.ts 12 new tests: tilde-prefix, glob patterns, relative paths, case-insensitivity, custom config, chain commands, backward compat

Design notes

  • Backward compatible: isAllowed(cmd) (2-arg form) still works — sensitive-path check only fires when projectRoot is provided.
  • Non-path tokens skipped: flags (-d), URLs (http://...), env vars ($HOME) are not resolved as paths.
  • Prefix boundary matching: ~/.ssh matches ~/.ssh/id_rsa but NOT ~/.ssh_extra/config.
  • Runs alongside RISKY_ARGS: both checks are applied; either can demote.

Acceptance criteria (from #259)

  • SENSITIVE_PATHS config layer (default list + user override)
  • Path-arg scanner in src/tools/shell.ts / parse.ts that flags any argv token resolving into the blocklist
  • Demotion fires in the same code path as RISKY_ARGS (chains + confirm gate work uniformly)
  • Tests: absolute paths, ~/-relative paths, cwd-relative paths, glob patterns, case-insensitive on win32
  • No false positives on legit project paths; ./src/.env.example should still trigger

…ine#259)

Path-argument scanner on top of the existing allowlist gate. When a
run_command or run_background call matches the allowlist but one of its
argv tokens resolves into a sensitive prefix or matches a sensitive
filename pattern, the call is demoted to the confirm gate — same code
path as RISKY_ARGS.

Default blocklist:
- Path prefixes: ~/.ssh, ~/.aws, ~/.gnupg, ~/.kube, /etc/shadow,
  /etc/sudoers
- Filename patterns (case-insensitive): *.env, *.env.*, *.key, *.pem,
  id_rsa*, id_ed25519*, *credentials*, *secret*

User-configurable via ReasonixConfig.sensitivePaths (prefixes + patterns).

Refs: OWASP AI Agent Cheat Sheet §1, §8
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 07f3f716db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tools/shell/parse.ts
if (chain === null) return isAllowed(cmd, extra);
return chainAllowed(chain, (seg) => isAllowed(seg, extra));
if (chain === null) return isAllowed(cmd, extra, projectRoot, sensitivePathConfig);
return chainAllowed(chain, (seg) => isAllowed(seg, extra, projectRoot, sensitivePathConfig));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include redirect targets in sensitive-path checks

Sensitive-path demotion can be bypassed with input redirection because isCommandAllowed parses chains/redirects and then re-checks each segment using only seg.argv.join(" "), which drops redirect targets. As a result, commands like cat < ~/.ssh/id_rsa (or head < /etc/shadow) are treated as plain cat/head and skip the confirm gate despite reading sensitive files.

Useful? React with 👍 / 👎.

Comment thread src/tools/shell/parse.ts Outdated

/** Ensure prefix matches only at directory boundaries (not mid-segment). */
function pathStartsWithPrefix(normalized: string, prefix: string): boolean {
return normalized === prefix || normalized.startsWith(`${prefix}/`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Normalize prefix boundary matching for Windows paths

The prefix boundary check hardcodes / as the separator, so on Windows normalized paths like C:\Users\me\.ssh\id_rsa will not satisfy startsWith(${prefix}/) when prefix is C:\Users\me\.ssh. This means child paths under sensitive prefixes are not demoted on Windows (only an exact directory match is), allowing the new guard to be bypassed on that platform.

Useful? React with 👍 / 👎.

@esengine
Copy link
Copy Markdown
Owner

Solid piece of work — OWASP § 1 / § 8 is the right framing, and demoting through the existing RISKY_ARGS code path keeps the gate uniform. Edge coverage (tilde expansion, glob, relative paths, chains, case folding, custom config) is thorough.

Two real fixes before merge:

  1. The user-config layer isn't actually wired up. ShellToolsOptions.sensitivePaths is plumbed, but src/code/setup.ts:44's registerShellTools call never reads config.sensitivePaths from readConfig() and forwards it — so the YAML override the PR documents is silently ignored and only the default list takes effect. Add the read in registerRooted and pass it through.

  2. Windows path separator. pathStartsWithPrefix does normalized.startsWith(${prefix}/) with a literal forward slash. After pathMod.normalize() on Windows the result uses backslashes, so C:\Users\bob\.ssh followed by \id_rsa won't match (Linux CI passes; real Windows misses). Use pathMod.sep or accept both separators.

After those two, happy to merge — the defaults, chain handling, and boundary-correct prefix match are otherwise exactly right.

esengine added a commit that referenced this pull request May 15, 2026
Recent PRs hit Windows-specific bugs that Ubuntu CI couldn't catch —
literal forward-slash separators in path comparisons (#911) and
`partial.startsWith("/")` as an "is absolute path" heuristic (#922) both
sailed through `ubuntu-latest` while shipping broken on the platform
that most of the user base actually runs.

Add `windows-latest` to the existing matrix so every PR exercises the
Windows code paths (taskkill tree-kill, MS Store node-shim detection,
path separator handling, etc.) before review. Job name now includes the
OS so the two runs are distinguishable in branch protection.

Also swap the hardcoded `/tmp/tau-dry.json` in the τ-bench dry-run for
`${{ runner.temp }}` — RUNNER_TEMP is set on every GitHub Actions runner
including Windows, where `/tmp` isn't a path.

Co-authored-by: reasonix <reasonix@deepseek.com>
1. Read config.sensitivePaths in registerRooted and forward to
   registerShellTools so the YAML override actually takes effect.
2. Use pathMod.sep in pathStartsWithPrefix instead of hardcoded '/'
   so Windows backslash paths match correctly.
@KuaaMU
Copy link
Copy Markdown
Contributor Author

KuaaMU commented May 15, 2026

Fixed both issues in the latest push:

  1. User-config wiring: Added readConfig() call in registerRooted and forwarded cfg.sensitivePaths to registerShellTools. The YAML override now takes effect.

  2. Windows path separator: Changed pathStartsWithPrefix to use pathMod.sep instead of hardcoded /, so C:\Users\bob\.ssh\id_rsa correctly matches prefix C:\Users\bob\.ssh on Windows.

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.

2 participants