Permission matcher treats .claude/worktrees/** as agent config, silently auto-approving source edits in manual mode

Status Open
Reported on v2.1.224
Maintainer reply None cached
Activity 1 comment · opened Aug 7, 2026

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

  1. In a git repo, have Claude Code create a worktree (it lands in .claude/worktrees/<name>/).
  2. Work inside that worktree in default (manual) permission mode.
  3. On the first edit prompt, choose "Yes, and allow Claude to edit its own settings for this session".
  4. 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. The
claude-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 untimestamped
permission-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:

  1. Exclude .claude/worktrees/** from Kdv / Ydv so the option isn't offered for worktree paths.
  2. Narrow Lyo / Nyo to exclude worktrees/, e.g. an accompanying deny rule.
  3. Store worktrees outside .claude/.

Independently worth doing:

  1. Record the accepted permission option and the installed rule in the transcript, so a session's

effective grants are auditable after the fact.

  1. Surface active session allow rules in the footer, or reword the label when the installed glob

would cover non-config paths.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗