Repository navigation
bug(safety): sensitive-path guard is bypassable via bash — deny list covers only read/write/edit #2349
Description
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
danielgap commentedon Oct 17, 2026
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_NAMESat line 1776,PATH_INPUT_KEYSat 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:
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 likeread. Small, self-contained slice.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. RunningSENSITIVE_PATH_PATTERNSover those targets makescat .envdeny likeread .env.One decision I'd like a maintainer to confirm before the
bashslice: 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.
codemodeandfetch_contentI'll check against their input shapes and either cover them or name them as an explicit follow-up.