Trust dialog on a chip's "Start with worktree" names the spawn_task cwd, not the worktree the session runs in

Status Open
Reported on v2.1.251
Maintainer reply None cached
Activity 0 comments · opened Aug 29, 2026

Summary

Clicking "Start with worktree" on a suggested-task chip can show a "Trust this workspace?" modal naming the cwd passed to spawn_task. The session then runs somewhere else. It runs in a worktree the click creates under the spawning repo's .claude/worktrees/.

I am not asking for the prompt to be removed. I traced why it fired, and it is correct under the current rules. The defect is the path it names. The user reads one directory, approves it, and the hook the banner cites then executes in a directory they were never shown.

The ask is one line: name the directory the session will run in.

Environment

  • Windows 11 Pro, Claude Code desktop app, build app-1.37937.3. Get-Process claude resolves to C:\Users\Scott\AppData\Local\AnthropicClaude\app-1.37937.3\claude.exe.
  • The desktop app draws this modal, not the CLI. grep -a -c for "Trust this workspace" and for "Execution allowed by" returns 0 against the 2.1.251 CLI binary, and both strings match in the app bundle's resources\ion-dist\i18n\en-US.json. The CLI has its own, differently worded prompt. CLI version 2.1.251 is listed for completeness only.
  • CLAUDE_CONFIG_DIR = C:\Users\Scott\.claude-account-2, set in-process. It is empty at User and Machine scope.
  • The repo is a git checkout with many linked worktrees. Main checkout at C:\Users\Scott\Code\MessageFoundry.

What happened

A session created 10 suggested-task chips with spawn_task, each carrying an explicit cwd. Nine produced a worktree and a session. Nothing on disk records a tenth attempt.

Clicking the default button, "Start with worktree", produced this modal:

Trust this workspace?
Claude Code may read, write, or execute files in this folder.
Only proceed if you trust this workspace.
C:\Users\Scott\Code\MessageFoundry-b1-1067-repo-governance
Read our security guide for more information.
Execution allowed by:
.claude/settings.json
[Cancel] [Trust workspace]

The path shown is the cwd passed to spawn_task. That directory is itself a linked worktree of the same repository, sited outside .claude/worktrees. Its .git is a 97-byte gitfile reading gitdir: C:/Users/Scott/Code/MessageFoundry/.git/worktrees/MessageFoundry-b1-1067-repo-governance. So this is not a cross-repository case.

The session that chip started ran in C:\Users\Scott\Code\MessageFoundry\.claude\worktrees\heuristic-nobel-5180df, created by the click at 15:09:28.

Why the prompt fired, and why that part is not the defect

Two measured facts explain the modal completely.

First, the named directory had no trust entry in the active config at click time. My own first reading of this was wrong, so I am stating the correction plainly. The snapshot C:\Users\Scott\.claude-account-2\backups\.claude.json.backup.1788034147474, written at 15:09:06 and 22 seconds before the first worktree existed, holds 23 project keys and none of them is that path in any spelling. grep -o '1067-repo-governance' on that file returns 0, and the positive control grep -c 'worktrees' returns 15, so the file is a complete snapshot and the absence is real. The hasTrustDialogAccepted: true I read afterwards was written by accepting the dialog.

The path was trusted, but in another account's config. C:\Users\Scott\.claude-account-4\.claude.json (49 projects, a different signed-in account) carries it true, along with 5 of the other 6 pre-existing chip sources. Trust is stored per CLAUDE_CONFIG_DIR. The account-4 config was last written at 14:30:34 and the account-2 config at 15:17:17. An mtime records a write, not a switch event, so read this as an inference: the account switch happened before the clicks and orphaned every earlier trust decision.

Second, the folder carries a tracked .claude/settings.json that the app reads as execution-granting, and that turns off inheritance. From the running bundle app-1.37937.3:

  • Me() scans exactly two candidates, both inside the target folder: .claude/settings.json and .claude/settings.local.json. User-level and managed settings are never scanned or listed.
  • Ae() marks a file execution-granting when it declares a non-empty hooks.<Event> array, or a permissions.allow rule for Bash or PowerShell, among other triggers.
  • The trust lookup X0t(e, t, r) takes that result as r. When r is true it returns an exact-key match only, skipping both the git-repo-root resolution and the walk up ancestor directories.

I reimplemented Me() and Ae() in Python and ran them against the real files. The named folder's settings.json is execution-granting twice over: a SessionStart hook and five PowerShell(...) allow rules. It has no settings.local.json, which is why the modal listed one file.

C:\Users\Scott\Code is itself a trusted key in the 15:09:06 baseline. Inference from the decompiled predicate: without the execution-granting file, the ancestor walk would have matched that parent and no modal would have appeared.

One theory is ruled out by construction. The config stores no fingerprint, checksum, hash, version or mtime for any settings file. A recursive key scan of .claude.json finds only hasTrustDialogAccepted, hasClaudeMdExternalIncludesApproved and hasClaudeMdExternalIncludesWarningShown as consent state, all plain booleans. Nothing exists that an edited settings file could invalidate.

The defect: consent names the source, execution happens in the worktree

The worktree is a checkout of the named directory's HEAD, so it carries the same tracked files. All three copies of .claude/settings.json are byte-identical: the main checkout, the named cwd, and the created worktree heuristic-nobel-5180df, each 1539 bytes, sha256 prefix 41b5db10fbea3353. The SessionStart hook the banner cites runs pwsh against scripts/hooks/seat-declare-prompt.ps1, and it runs in the worktree.

This is not avoidable per repo, either. git ls-files .claude/ returns exactly one path, .claude/settings.json, and git check-ignore -v .claude/settings.local.json returns .gitignore:167:/.claude/*. Every worktree of this repo is born with the hook-bearing file and without the local one.

So the executable content consented to is the content that executes. What does not match is the location. The user approves one path and the code runs in another, whose name the dialog never showed.

The flow also records consent it did not need. No session ran with the named directory as its cwd: extracting every distinct "cwd" value from all transcripts under the active account's projects tree yields more than 30 paths, and that one is not among them. The one use the flow made of the folder was reading its git HEAD to pick a branch base. git reflog show claude/heuristic-nobel-5180df reads branch: Created from 5c889a8220299a3a27a6d1f88b3417bf62b28010, which is the HEAD git worktree list --porcelain reports for that directory. Six more of the nine branches match their chip's cwd HEAD, sha for sha. The remaining two read Created from refs/remotes/origin/main.

Reading a git HEAD should not require an execution grant. One caveat I checked: git worktree add can fire a post-checkout hook, but that hook lives in the main repo's shared .git/hooks, not in the passed cwd's settings, so trusting the passed cwd does not gate it.

Measurements

All of this was measured on disk after the event, on 2026-08-29.

Nine worktrees were created in a 2 minute 24 second burst, from 15:09:28 to 15:11:52 local. Run the listing with a filter, because the unfiltered command covers every registered worktree of this repo and git worktree list reports 182 entries:

Get-ChildItem "C:\Users\Scott\Code\MessageFoundry\.git\worktrees" -Directory |
  Where-Object { $_.CreationTime -ge [datetime]'2026-08-29 15:09' }

That selects nine admin directories. The next-newest outside the window is 14:31:04, 38 minutes earlier.

Each click created a worktree and a branch of the same name. git reflog show claude/heuristic-nobel-5180df --date=iso records the branch created at 15:09:28 from 5c889a822, and the other eight match their directories the same way. Branches have moved since: git branch --show-current in that worktree returns b1-asvs-falsezero today, so a check now shows a different name.

Every session ran in its created worktree. C:\Users\Scott\.claude-account-2\projects\ gained exactly nine directories in the window, from 15:09:35 to 15:12:00, all named C--Users-Scott-Code-MessageFoundry--claude-worktrees-<name>. A filter for *b1-1067* over that directory returns nothing.

Sixteen trust keys were added in about three minutes, and every one was born true. The backups directory holds five snapshots spanning the window (15:09:06, 15:10:06, 15:11:08, 15:12:11, 15:12:58). Diffing the projects maps in order takes the file from 23 keys to 39. Seven of the additions are pre-existing directories that were passed as cwd; nine are worktrees the clicks created. So seven chips wrote two entries, the source then its worktree, and two chips wrote only a worktree entry. Those two are the ones whose branch came from refs/remotes/origin/main rather than a source HEAD.

The order within each diff is source before its own worktree, not a strict alternation. The 15:10:06 diff adds, in file order: MessageFoundry-b1-1067-repo-governance, heuristic-nobel-5180df, peaceful-faraday-0d02fd, lucid-bouman-ac5adb, sleepy-bose-41a4e8.

The app writes a trust record for a worktree it creates, rather than relying on inheritance. Each created worktree first appears in the snapshot written seconds after its directory's creation time. The mechanism is already visible in the product's own log: Auto-trusted worktree with key "<path>" (inherited from <repo>) appears in this machine's main.log for earlier worktrees, for example at 2026-08-27 11:43:42, and #78212 quotes the same line. Whatever writes that record is the code that should also decide whether the source cwd needs a prompt.

The trust map grows quickly and permanently. Worktree-path keys went from 14 to 29 in three minutes, all trusted. Of the 32 trusted entries in the current file, 22 point at directories that still exist and 10 do not, so grants appear to accumulate for deleted paths.

What I could not determine

How many modals appeared. One is recorded here, for one path. Nothing on disk records a prompt event: grep -rl for "Trust this workspace", "trustDialog" and "Execution allowed by" across projects\, sessions\ and history.jsonl matches only this investigation's own transcripts. The write pattern predicts seven modals, one per source directory with no entry in the active config, and none for the nine created worktrees.

Which path the chip-launch code checks. I traced the folder-picker and branch-switch call sites, and both pass the selected folder, which matches the path displayed. I did not trace the chip-launch call site. The decisive instrument is the app's own LocalSessions.checkTrust: cwd= line, which prints the exact string checked. It was unavailable: C:\Users\Scott\AppData\Local\Claude\Logs\main.log stops at 2026-08-28 21:41, and I found no log written on 2026-08-29.

Where the tenth chip went. Nine worktrees, nine branches, nine transcripts, for ten chips.

Whether the trust lookup normalizes path spellings. Cr() and Sr() live in a chunk I did not resolve. It is not a factor here, since the modal used the trusted backslash spelling.

Reproduction

I have not re-run this, so treat it as a sketch. Two conditions matter, one for the modal and one for the mismatch.

  1. Use a git repo whose tracked .claude/settings.json declares a hook or a Bash/PowerShell permissions.allow rule. Because it is tracked, every worktree carries it.
  2. Create a linked worktree at a sibling path outside .claude/worktrees.
  3. Confirm that sibling has no key in projects in the active CLAUDE_CONFIG_DIR's .claude.json.
  4. Get a session to emit a chip with cwd set to that sibling. spawn_task is model-invoked, so there is no menu item for this; a maintainer needs a prompt that makes the model file background tasks with explicit cwds. I did not recover the wording used here and can capture it on the next occurrence.
  5. Click "Start with worktree", the chip's default button.
  6. Observe the modal naming the sibling. Then check the session's cwd and the new keys in projects.
  7. Control: click a second chip whose cwd already has hasTrustDialogAccepted: true in the active config. My prediction is no modal, while a fresh worktree is still created and still gets its own trusted entry.

Step 7 is the arm that separates a chip-flow defect from ordinary unknown-directory behaviour. I will run it and post the result if that helps triage.

Prior art

Across fourteen searches of this repository I found no issue reporting a trust dialog that names a directory other than the one the session runs in. The searches covered "trust this workspace", "trust dialog", "trust prompt", hasTrustDialogAccepted, "worktree trust", "Start with worktree", "Execution allowed by", spawn_task, "suggested task chip", worktree plus trust in title, and a dated listing of trust issues.

Adjacent issues, and why each is not this one:

  • #86691 (open) asks that a chip's cwd drive where the worktree is created. That is the same placement behaviour I measured, framed as a design gap. It says nothing about trust and does not address which directory the modal names.
  • #78212 (open) documents the auto-trust mechanism for worktrees the app creates, quotes the log line above, and records the case where the app declines because the hook came from a repo-committed settings tier. It is the closest mechanism reference. Different trigger and different symptom.
  • #77263 (open) and #64605 (closed) report the opposite outcome: chip sessions running on the primary checkout with no worktree created. That looks behaviourally stale against what I measured.
  • #23109 (closed) and #21448 (closed, labelled duplicate) ask for parent-trust inheritance for CLI-created worktrees. #23109 records #993 and #21283 as auto-closed predecessors. I am not re-filing that ask; see the note under Asks.
  • #87012 (open) states that 2.1.232 removed trust inheritance from parent directories and calls that change correct. #86824 (closed) is the adjacent report. The shipped app still contains an ancestor walk in the trust lookup, skipped only when the folder carries execution-granting settings, so "inheritance was removed" is not a flat rule and does not by itself explain this dialog.
  • #88418 (open, mine) reports one directory stored under several path spellings with disagreeing trust flags. No spelling collision explains prompting for a path the session never uses, and the modal here used the trusted backslash spelling.
  • #54628 (closed) quotes the same "Execution allowed by:" footer for a remote workspace, which corroborates the modal wording. Different cause.
  • Same button, different defects, listed so they are not confused with this one: #90146, #89940, #81213, #89698. None mentions trust.

Asks

  1. Name the directory the session will run in. This is the primary ask and it has no prior art across fourteen searches.
  2. Do not write a permanent trust entry for a directory the flow only reads a git HEAD from.

I am not asking that the prompt be suppressed because the user created the workspace. That ask has four closed predecessors (#993, #21283, #21448, #23109), and the app already auto-trusts worktrees it creates.

Secondary observations

Cross-account trust orphaning is what made this prompt fire. Trust lives under CLAUDE_CONFIG_DIR, so switching accounts silently drops every prior trust decision and every workspace re-prompts. That is broader than worktrees or chips, and I am happy to file it separately if you want it tracked.

Project keys are raw path strings with no normalization. The active config holds three spellings of one directory with independent trust values: C:\Users\Scott\Code\MessageFoundry true, C:/Users/Scott/Code/MessageFoundry false, c:/Users/Scott/Code/MessageFoundry false. The split is systematic, with all 32 backslash keys trusted and all 7 forward-slash keys not. This did not cause the incident.

One instrument warning for anyone inspecting these files. The case-colliding duplicate keys make .claude.json unreadable by PowerShell's ConvertFrom-Json, which errors and, in a pipeline, yields a silent zero. Python's json.load with an object_pairs_hook reads it correctly.

View original on GitHub ↗