Skip to content

bug(safety): sensitive-path guard is bypassable via bash — deny list covers only read/write/edit #2349

Open

Description

@maiconcrespo

Before submitting

  • I searched open and closed issues and did not find a report of this problem.
  • I reviewed this report and removed credentials, tokens, private paths, hostnames, and other sensitive data.

Problem

The sensitive-path deny list in extensions/gentle-ai.ts is applied only to the tools named read, write, and edit (PATH_GUARDED_TOOL_NAMES, line 1776), and it inspects only the tool name plus a fixed set of path-like input keys (PATH_INPUT_KEYS, line 1777). Any tool that reaches the same file through a different input key is not checked at all.

The important case is bash: evaluateSensitivePathTool("bash", input) returns undefined unconditionally, because "bash" is not in PATH_GUARDED_TOOL_NAMES and command is not in PATH_INPUT_KEYS. So the deny list protects a sensitive file when the model uses read, but not when the model uses bash. A single bash call such as cat <path> returns exactly the content the guard exists to protect. There is no scrub or redaction pass over tool results afterwards, so the content reaches the session transcript through a route the safety policy never evaluates.

The gap is in the guard's coverage, not in its matching rules. It affects bash and every other tool whose input shape is outside PATH_INPUT_KEYS (grep, find, ls, codemode, fetch_content, ...).

This report is scoped to the bypass. It deliberately does not propose a transcript-redaction design; how to keep secret bytes out of a persisted transcript is a separate design question.

Related but distinct: #838 tracks the opposite defect (*.example / *.sample / *.template / *.dist templates being blocked as sensitive).

Steps to reproduce

No repository, credentials, or live service are needed. The guard was reproduced in isolation by copying PATH_GUARDED_TOOL_NAMES, PATH_INPUT_KEYS, SENSITIVE_PATH_PATTERNS, normalizePolicyPath, isSensitivePath, collectPathInputs, and evaluateSensitivePathTool verbatim from the installed extensions/gentle-ai.ts:

const PATH_GUARDED_TOOL_NAMES = new Set(["read", "write", "edit"]);
const PATH_INPUT_KEYS = new Set(["path","paths","file","files","filePath","filePaths"]);
const SENSITIVE_PATH_PATTERNS = [
  /(^|\/)\.ssh(?:\/|$)/,
  /(^|\/)\.credentials(?:\/|$)/,
  /(^|\/)library\/keychains(?:\/|$)/,
  /(^|\/)\.aws\/credentials$/,
  /(^|\/)\.config\/gh\/hosts\.ya?ml$/,
  /(^|\/)secrets(?:\/|$)/,
  /(^|\/)\.env(?:$|[./_-])/,
  /\.(?:pem|key|p12|pfx)$/,
];
const normalizePolicyPath = (v) => v.trim().replace(/^~(?=\/|$)/, "~").replace(/\\/g,"/").toLowerCase();
const isSensitivePath = (v) => SENSITIVE_PATH_PATTERNS.some((p) => p.test(normalizePolicyPath(v)));
function collectPathInputs(value, key) {
  if (typeof value === "string") return key && PATH_INPUT_KEYS.has(key) ? [value] : [];
  if (Array.isArray(value)) return value.flatMap((i) => collectPathInputs(i, key));
  if (typeof value !== "object" || value === null) return [];
  return Object.entries(value).flatMap(([k, v]) => collectPathInputs(v, k));
}
const evaluateSensitivePathTool = (toolName, input) => {
  if (!PATH_GUARDED_TOOL_NAMES.has(toolName)) return undefined;
  const hit = collectPathInputs(input).find(isSensitivePath);
  return hit ? { block: true, path: hit } : undefined;
};

Then every call below is a single evaluateSensitivePathTool(toolName, input) invocation, with synthetic paths only:

evaluateSensitivePathTool("read", { path: ".env" });
evaluateSensitivePathTool("bash", { command: "cat .env" });
evaluateSensitivePathTool("bash", { command: "cat ~/.ssh/id_rsa" });
evaluateSensitivePathTool("bash", { command: "cat ~/.aws/credentials" });

To confirm against a real install rather than the isolated copy, point read at a sensitive file, then point bash at the same file with an equivalent command.

Expected and actual behavior

Expected: a sensitive path is denied regardless of which tool reaches it, or at minimum the guard evaluates the resolved file a bash command targets, so cat <.env> is blocked exactly like read <.env>.

Actual: the same file is blocked through read and allowed through bash. The block is a function of the tool the model happens to pick, not of the file being protected.

gentle-pi version

4.3.0

Pi version

1.2.0

Operating system

Linux

Relevant logs or error output (optional)

--- A. read/write/edit path inputs (the only guarded surface) ---
BLOCK  read   .env
BLOCK  read   /app/.env.production
BLOCK  read   .env.example          (known bug #838)
allow  read   .envrc
allow  read   config/envvars.json
BLOCK  edit   secrets/token.txt

--- B. bash tool (not in PATH_GUARDED_TOOL_NAMES) ---
allow  bash   cat .env
allow  bash   cat /app/.env.production
allow  bash   cat ~/.ssh/id_rsa
allow  bash   cat ~/.aws/credentials
allow  bash   printenv > /tmp/dump.txt

--- C. other tools that accept a path and are not guarded ---
allow  grep   { path: ".env" }
allow  find   { path: ".env" }
allow  ls     { path: ".env" }
allow  codemode
allow  fetch_content

Activity

  1. danielgap commented on Oct 17, 2026

    @danielgap
    Contributor

    Thanks for the precise report, @maiconcrespo. The isolated reproduction and the A/B/C matrix make the gap unambiguous, and I confirmed both anchors on current main (PATH_GUARDED_TOOL_NAMES at line 1776, PATH_INPUT_KEYS at 1777).

    I'd like to take this one. It overlaps with work I already have in flight under #1305, so the plan reuses that instead of building a second guard:

    1. Path tools (grep, find, ls). fix(safety): require consent for path tools outside the session worktree (#1305) #1610 already classifies every input key of those tools' real Pi schemas as path-bearing or not, with a drift test. The sensitive-path deny list can run over the same classification, so these tools get blocked exactly like read. Small, self-contained slice.
    2. bash. The bug(harness) Agent left the current project directory (sibling project) without asking for permission #1305 bash fence (slice 3, follows fix(safety): require consent for path tools outside the session worktree (#1305) #1610) extracts the file targets a command reaches. Running SENSITIVE_PATH_PATTERNS over those targets makes cat .env deny like read .env.

    One decision I'd like a maintainer to confirm before the bash slice: shell parsing can't prove every target ($VAR, $(...), eval, python -c, base64 tricks). For commands whose targets can't be proven, should the deny list (a) stay silent and treat this as best-effort coverage of the direct forms, or (b) fail closed and block? (b) is safer but will refuse a lot of legitimate commands. My suggestion is (a) for this issue, with the limit documented, since a complete answer needs the transcript-side design you deliberately scoped out.

    I'll keep transcript redaction and #838 out of scope as the report asks. codemode and fetch_content I'll check against their input shapes and either cover them or name them as an explicit follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    status:approvedIssue approved by maintainer; PR may be opened

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions