fix(permissions): match normalized paths for read and bash deny rules
Issue for this PR
Closes #1496
Type of change
- Bug fix
- New feature
- Refactor / code improvement
- Documentation
What does this PR do?
Why: permission deny rules written with absolute paths could be sidestepped because the read and bash tools matched rules against whatever spelling the model happened to use (relative path, .., quoted operand, path after cd, or a bare ls).
Root cause: read requested permission only for the worktree-relative spelling, and bash matched rules against the raw command text. Absolute rules therefore never saw an equivalent relative spelling. Separately, the legacy permission ask returned at the first ask result, so a later denied command in the same request could be skipped once the earlier prompt was approved.
Before -> after:
readandbashnow submit the authored spelling plus normalized absolute aliases (literal and quoted operands, straight-linecd, relativeworkdir,lsincluding barels; symlink-resolved targets forread).- Aliases are used only for
deny/askrules.allowrules still match only the authored spelling, so normalization can tighten a decision but never broadens an allow. Rule precedence (last match wins) is unchanged. - Independent invocations (same text in different directories) are each evaluated; one allowed spelling cannot mask another denied one.
- Legacy
asknow rejects any denied pattern before prompting; both permission implementations share one evaluator. - Background
&lists no longer propagatecdto later commands.
Review notes: an independent review found that an allow rule could match the normalized spelling (and quoting/../background cases made that unsafe); this was fixed by making aliases deny/ask-only, with regression tests.
Scope and limits
- Bash permission patterns are best-effort matching on command text. They are not a security boundary.
- Normalization only ever tightens decisions: aliases apply to
deny/askrules, never toallowrules, and the result is the strictest of the authored and alias-aware match. Rules remain last-match-wins, so a later authored allow (including an "always" approval) still overrides an earlier rule for the same command. - Filesystem isolation (a sandbox or OS-level permissions) is the boundary for untrusted commands. Structural enforcement is out of scope for this change.
readadditionally matches the symlink-resolved real path of its target fordeny/askrules.- Remaining reported bypass classes, intentionally not chased here: shell expansions (variables,
CDPATH,HOME, globs), paths used only by redirections, wrapper commands andsh -c, directory-stack builtins, loop iterations and repeated failingcd, symlinks traversed before..collapsing, reference-file operands, and further operand or whitespace spelling variants beyond those enumerated. - Not changed: the debug agent handler still uses the scalar evaluator.
How did you verify your code works?
From packages/opencode:
bun test test/tool/read.test.ts test/tool/bash.test.ts test/permission: 226 pass, 0 fail (new tests: read 3, bash 14, permission 9+; red before fix).bun test test/tool test/permission: 680 pass, 1 fail (detectDataToolsinproject-scan.test.ts, probes external tools, unrelated to touched modules).bun test test/permission-yolo.test.ts test/permission-task.test.ts test/agent/agent.test.ts test/file/security-e2e.test.ts test/server/httpapi-session.test.ts test/cli/run: 470 pass, 0 fail.bun run typecheck: clean.git diff --check: clean.- Marker audit against the v1.4.14 baseline tag:
permission/evaluate.ts,permission/index.ts,tool/bash.ts,tool/read.tsexist there; all changed lines are insidealtimate_changemarkers (a pre-existing nested marker inbash.tsis unchanged).permission/next.tsis not in the tag and is marked as well.
Screenshots / recordings
N/A (no UI change).
Checklist
- I have tested my changes locally
- I have not included unrelated changes in this PR
🤖 Generated with Claude Code
[!NOTE]
High Risk
Changes core permission matching for read and bash—security-sensitive paths where incorrect alias or precedence logic could wrongly allow or deny access.
Overview
Closes a permission bypass where absolute-path deny rules could be evaded because read and bash only matched the authored spelling (relative paths, .., quotes, post-cd commands, etc.).
A shared permission/evaluate module now drives both Effect and legacy permission flows. Tools attach metadata.permissionAliases so deny/ask rules can match normalized equivalents; allow rules still match only the authored pattern, so aliases can only tighten outcomes. Legacy ask evaluates denies first (including multi-pattern requests) via evaluateRequestPatterns.
read resolves paths against the instance, includes symlink-resolved spellings in aliases, and always requests permission on the worktree-relative pattern. bash parses static shell with tree-sitter to emit command variant aliases (operands, cd chains, ls, mixed quoting, caps on explosion), resolves workdir from the instance root, and passes aliases on bash permission requests.
Regression tests cover alias precedence, separate invocations, and approval behavior when spellings differ.
Reviewed by Cursor Bugbot for commit 6c17622f8deccc1ee22d50b30a2a16ff12b8009e. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Fixes a bypass where permission deny rules written with absolute paths could be sidestepped because read and bash matched rules against whatever spelling the model happened to use.
read and bash now evaluate rules against the authored spelling plus normalized absolute aliases (quoted, numeric, backslash-escaped, and mixed operand spellings, cd sequences, relative workdir, bare ls, symlink-resolved targets for read, and whitespace-collapsed command text). Aliases apply only to deny/ask rules, so normalization can tighten a decision but never lets an allow rule match a spelling it did not match before. Legacy ask now rejects any denied pattern before prompting, and cd stays local to subshells, substitutions, and pipelines while background & lists no longer propagate it.
Known limits
- Static matching only: shell expansions, redirection-only paths, wrapper commands like
sh -c, failedcd, and..through a symlink are not normalized.
Summary by CodeRabbit
- Bug Fixes
- Permission checks now account for relative paths, absolute paths, and paths resolved through symlinks when accessing files or running shell commands.
- Deny rules take precedence when equivalent path spellings match different rules; allow rules do not apply solely through an alternate spelling.
- Shell permission checks track directory changes across sequential commands.
- Relative working directories are resolved from the current workspace.
- Permission requests evaluate all requested patterns and prioritize denied results.