Permission matcher treats .claude/worktrees/** as agent config, silently auto-approving source edits in manual mode
Version: 2.1.224 (native install, ~/.local/share/claude/versions/2.1.224)
Platform: darwin 25.5.0, arm64
Summary
The "Yes, and allow Claude to edit its own settings for this session" permission option installs a
session allow rule for /.claude/**. Claude Code also creates git worktrees inside.claude/worktrees/. Those two facts collide: accepting that option grants unprompted write access
to every source file in every worktree of the project, while the footer continues to read
"manual mode".
In one session this auto-approved 23 edits to production Java (a payments module) with no prompt.
Repro
- In a git repo, have Claude Code create a worktree (it lands in
.claude/worktrees/<name>/). - Work inside that worktree in
default(manual) permission mode. - On the first edit prompt, choose "Yes, and allow Claude to edit its own settings for this session".
- Every subsequent edit to any file under
.claude/worktrees/<name>/— i.e. the whole source tree —
applies with no prompt. The footer still says "manual mode".
Expected
That option should grant config paths only. Edits to ordinary project files inside a worktree should
still prompt in manual mode.
Actual
The grant covers the entire worktree. Mode is never changed, so nothing in the UI indicates the
change in behavior.
Root cause (from the 2.1.224 bundle)
The option is gated on a project-relative .claude prefix:
function Kdv(e){ let t=Zi(e), r=Zi(`${En()}/.claude`), n=ay(t), o=ay(r);
return n.startsWith(o+ftr.sep.toLowerCase()) || n.startsWith(o+"/") }
Lyo = "/.claude/**" // project scope
Nyo = "~/.claude/**" // global scope
if ((u||d) && r!=="read") a.push({
label: "Yes, and allow Claude to edit its own settings for this session",
value: "yes-claude-folder",
option: { type:"accept-session", scope: d ? "global-claude-folder" : "claude-folder" }})
Worktree creation targets the same subtree, and is not user-relocatable:
"In a git repository: creates a new git worktree inside .claude/worktrees/"
"A repository-committed symlink at .claude, .claude/worktrees, or .claude/worktrees/<name> could redirect worktree creation outside the repository. Remove the symlink and retry."
So /.claude/** necessarily covers all worktree source files. worktree.baseRef controls the base
ref, not the path.
The product already documents the correct reading. From the model-facing rules in the same binary:
"Not every path under.claude/is config:.claude/worktrees/<name>/is where Claude Code stores git worktrees, so treat files under it as ordinary project files (though a nested config path from the list above inside a worktree is still Self-Modification)"
The guidance string draws the distinction; Kdv / Lyo do not.
Why it's invisible in the UI
cth() returns early for the claude-folder scope, before lOr() runs, so no setMode is emitted:
case "accept-session": {
if (e.scope==="claude-folder" || e.scope==="global-claude-folder") {
let o = e.scope==="global-claude-folder" ? Nyo : Lyo;
return { behavior:"allow", updatedInput:t.input,
permissionUpdates:[{ type:"addRules", rules:[{toolName:Ol, ruleContent:o}],
behavior:"allow", destination:"session" }] }
}
return { behavior:"allow", updatedInput:t.input,
permissionUpdates: lOr(t.filePath, t.operationType, r) }
}
The other accept-session branch does emit setMode acceptEdits, so the footer changes there. Theclaude-folder branch leaves permissionMode at default, whose indicator is "manual mode":
$wc = { default: { indicator:"manual mode", external:"default" },
acceptEdits: { indicator:"accept edits" }, ... }
Contributing factors (not separate bugs):
- The label says "edit its own settings" for a grant that reached
.../rest/api/*Controller.java. - The dialog choice is not recorded in the session transcript. The only permission-related key
written is permissionMode; there is no record of which option was accepted or what rule it
installed, so this is not diagnosable from logs.
- Shift+Tab does not clear it — cycling modes only touches
permissionMode, while session rules and
addDirectories grants persist for the life of the session.
Evidence
One session, three segments. Segment 3 ran in default throughout (dense untimestampedpermission-mode snapshots every ~15-25 entries, all default; every edit bracketed by default on
both sides, zero spanning a boundary).
Approval latency in segment 3, split by path — this is the discriminator:
[worktree] <Entity>.java 0.02s auto
[worktree] <Reconciler>.java 0.04s auto
[worktree] <Controller>.java 0.05s auto
... (23 more, median 0.04s)
[other repo] <changeset>.sql 6.01s human approved
[other repo] <changeset>.sql 45.37s human approved
[other repo] <changeset>.sql 118.21s human approved
Aggregate for the session:
| mode | under <project>/.claude/ | n | median latency | <0.5s |
|---|---|---|---|---|
| default | yes | 24 | 0.04s | 23/24 |
| default | no | 10 | 0.05s | 6/10 |
| acceptEdits | yes | 124 | 0.05s | 124/124 |
| acceptEdits | no | 7 | 0.04s | 6/7 |
Same session, same mode: paths inside .claude/worktrees/ auto-approve at ~0.04s; paths in another
repo take seconds to minutes of human review. 23 auto-approved edits across 8 files, all production
source and tests.
Inferred, not observed: that the claude-folder option was the one accepted. The choice isn't
logged. Reached by elimination — the plain accept-session branch would have set acceptEdits and
changed the footer, and a bare addDirectories grant governs whether a write is permitted, not
whether it is auto-approved. claude-folder is the only branch producing "auto-approved while
still in default".
Suggested fix
Any one of:
- Exclude
.claude/worktrees/**fromKdv/Ydvso the option isn't offered for worktree paths. - Narrow
Lyo/Nyoto excludeworktrees/, e.g. an accompanying deny rule. - Store worktrees outside
.claude/.
Independently worth doing:
- Record the accepted permission option and the installed rule in the transcript, so a session's
effective grants are auditable after the fact.
- Surface active session allow rules in the footer, or reword the label when the installed glob
would cover non-config paths.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗