Skip to content

fix: pre-trust worktrees in the config file Claude actually reads; say why a task is blocked - #825

Merged
bborn merged 2 commits into
mainfrom
fix/trust-prompt-and-placement-note
Oct 3, 2026
Merged

bborn merged 2 commits into
mainfrom
fix/trust-prompt-and-placement-note

Conversation

@bborn

@bborn bborn commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

What happened

A task routed to a non-default Claude profile went to blocked about 25s after it started. ty show printed:

Ran on:   local
Because:  no host in ~/.config/on/hosts.yaml serves myapp (4 hosts in inventory)

That line reads like the reason for the block. It is only the placement resolver keeping the task local, which was correct: none of the hosts has a checkout of the project. The real cause was Claude Code's "Quick safety check" workspace trust dialog in the fresh worktree.

Root cause (verified)

  • The task log shows Routed to Claude profile ~/.claude-work (by plugin "claude-profile-router"), and the task's claude process had CLAUDE_CONFIG_DIR=~/.claude-work.
  • ensureProjectTrusted writes to ClaudeConfigFilePath(dir), which returned dir + ".json", so ~/.claude-work.json. That file holds only TaskYou's own hasTrustDialogAccepted / hasCompletedProjectOnboarding entries.
  • Claude Code 2.1.288 resolves its global config as $CLAUDE_CONFIG_DIR/.claude.json. It uses ~/.claude.json only when the variable is unset, and a .config.json in the config dir takes precedence (To()/Far() in the binary). So the pre-trust went to a file Claude never reads.
  • Claude's backups of ~/.claude-work/.claude.json show the project's hasTrustDialogAccepted stayed false until the dialog was answered by hand.
  • The repro used a scratch repo with a nested .task-worktrees/wt, a .claude symlink, settings.local.json pre-approving Read(.claude/attachments/**), and a non-default CLAUDE_CONFIG_DIR. Results:
    • Trust only in <dir>.json (the old ty behaviour): dialog.
    • Trust in <dir>/.claude.json for the main repo path: no dialog, with the settings.local.json permission present.
    • Trust written by this branch's ensureProjectTrusted: no dialog.

So the pre-approved permission in settings.local.json is not the trigger. The dialog only lists it. Writing the trust to the right file is enough. The default profile was never affected, because ~/.claude + .json is ~/.claude.json, and ty never exports the default dir.

Changes

  • ClaudeConfigFilePath returns <dir>/.claude.json for a non-default dir, a legacy <dir>/.config.json when that exists, and ~/.claude.json for the default. This also fixes copyMCPConfig and purge-claude-config --config-dir for profiles, which were reading and writing the same dead file.
  • Trust prompt detection: added the current dialog wording to blockingPromptPatterns. A task parked there now records Waiting on Claude Code's workspace trust prompt — choose "Yes, I trust this folder" to let the task start instead of the generic Executor is waiting on a dialog.
  • ty show:
    • A new Blocked: line shows the reason and evidence from the transition that blocked the task.
    • Placement prints as Placement: local — <reason> (or Placement: <host> — <reason>) in place of the Ran on:/Because: pair, so it no longer reads as a block reason.
    • --json gains blocked_reason and blocked_detail. ran_on/ran_on_reason are unchanged.
  • Placement (ty-on): I read the resolver and confirmed it only offers or chooses hosts with a non-blank checkout of the project. It falls back to local when there is no inventory, no serving host, or the probe fails, and core falls back to local when no handler is installed or a handler fails. Existing tests covered most of this. I added regression tests for that inventory shape (prefix-similar project names, a blank checkout, a host with no repos) and for "no inventory configured → local".
  • QA stress script: corrected comments that documented the old path mismatch as a constraint.

Rendered ty show (branch binary against a DB copy, with a trust-prompt block event added):

Status:   blocked
Blocked:  the executor is waiting on a dialog and cannot make progress
          Waiting on Claude Code's workspace trust prompt — choose "Yes, I trust this folder" to let the task start
...
Placement: local — no host in ~/.config/on/hosts.yaml serves myapp (4 hosts in inventory)

Tests

  • New: TestClaudeConfigFilePath, a stricter custom-dir subtest in TestEnsureProjectTrusted, TestDetectBlockingPromptNamesTheTrustDialog (real 2.1.288 capture), TestPlacementLine, TestBlockReason*, and ty-on TestResolveNeverPlacesOnAHostThatDoesNotServeTheProject and TestResolveWithNoInventoryRunsLocal.
  • go test ./... fails the same 7 tests on this branch as on origin/main: the executor phantom-worktree and remote-worktree script tests, reaper TestLivePanePIDs_HonorsAgentSocket, and ui TestDetectMimeType_FixesPreviouslyBrokenFiles. They are environment-specific on this machine and unrelated to this change. ty-on (extensions/ty-on, its own module) passes. The new tests pass under -race.

Not covered

  • An explicit manual placement (ty place <id> <host>, --on <host>) is still the user's choice. It is not checked against the inventory, only by the host preflight at launch.
  • Existing ~/<profile>.json files that ty wrote under the old path are left in place. They are harmless.

🤖 Generated with Claude Code

… reads

A task routed to a non-default Claude profile went to blocked ~25s after
start, on Claude Code's "Quick safety check" workspace trust dialog.
`ty show` printed only "Because: no host in hosts.yaml serves <project>",
which read as the cause of the block. It wasn't.

Root cause: the claude-profile-router plugin routed the task to
CLAUDE_CONFIG_DIR=<dir>. ensureProjectTrusted wrote the trust to
ClaudeConfigFilePath(dir) == "<dir>.json", but Claude launched with
CLAUDE_CONFIG_DIR=<dir> reads "<dir>/.claude.json". So the pre-trust went
to a file Claude never opens, and every task routed to a non-default
profile reached the trust dialog. ClaudeConfigFilePath now returns
<dir>/.claude.json for a non-default dir (and a legacy <dir>/.config.json
when present, as Claude does). The default ~/.claude still maps to
~/.claude.json, because ty never exports the default dir.

Also:
- Name the current trust dialog wording in the blocking-prompt detector,
  so a task parked there says "Waiting on Claude Code's workspace trust
  prompt" instead of the generic "waiting on a dialog".
- ty show: print the reason from the transition that blocked the task
  ("Blocked:"), and print placement as "Placement: local — <reason>" in
  place of the "Ran on:"/"Because:" pair. JSON gains blocked_reason and
  blocked_detail.
- ty-on: regression tests that a host which doesn't serve the project
  (prefix-similar names, blank checkout) is never chosen or offered, and
  that a missing inventory resolves local.
- Correct the QA stress script's notes, which documented the old path
  mismatch as a constraint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bborn
bborn force-pushed the fix/trust-prompt-and-placement-note branch from 63117c9 to 94c76d1 Compare October 3, 2026 09:19
…der dash

TestFinishedRemoteTaskSessionsAreEnded has failed on CI since #823 while
passing on macOS and Arch. The fake ssh ran
`FAKE_HOST=$host PATH=... eval "sh -c $inner"`. bash passes prefix
assignments on to what eval runs, but dash (Ubuntu's /bin/sh) treats eval
as a special builtin and keeps them in the current shell unexported, so
the fake tmux saw an empty FAKE_HOST, matched no windows, and the sweep
ended nothing. Export them instead. Test-only; the sweep's own script
is POSIX and unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bborn
bborn merged commit 586bf68 into main Oct 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant