Repository navigation
[Bug]: Worktree creation fails at configureBaseRef when .git/config is locked and leaves an orphan worktree + branch #11735
Description
Activity
Thanks for the deterministic repro and the lock vs. orphan split. This checks out on current
main(6f00d3881) and is a real server bug, not a wash of #4380 or a duplicate of the other worktree-create failures.What’s going wrong
GitVcsDriver.createWorktreeinapps/server/src/vcs/GitVcsDriverCore.tsdoes three things after a successful add:git worktree add -b <new> <path> <ref>— this is what creates the directory andt3code/<id>.- Submodule
update --init --recursive— already best-effort; failure does not fail the call. git config branch.<new>.gh-merge-base <base>in the main repo (GitVcsDriver.createWorktree.configureBaseRef) — hard-required. No retry, no rollback.
That last write needs
.git/config.lock. Git usesO_CREAT|O_EXCL, so any file at that path exits 255 withcould not lock config file .git/config: File exists. Your hand repro (touch .git/config.lockor the 0-byte mode0444bubblewrap placeholder) matches that.git worktree add -bwithout--trackcan succeed without taking that lock; the explicitgit configis the first write that needs it. So the add commits, then the metadata write fails, and the caller sees:Git command failed in GitVcsDriver.createWorktree.configureBaseRef (...): Git command exited with a non-zero status.The span for
configureBaseRefstill says Success becauseexecuteGitruns the child withallowNonZeroExit: trueand only fails after the span.GitCommandErrorstoresstderrLengthand a staticdetail, not git’s stderr — same funnel as #4380.The write is not required for a usable worktree.
resolveBaseBranchForNoUpstreamalready triesgh-merge-base, then the default branch, thenmain/master.Why the orphans stay
New-thread bootstrap in
apps/server/src/ws.tscreates the thread, then callscreateWorktree.onWorktreeClaimedruns after the add and beforeconfigureBaseRef. On failure,cleanupCreatedThread()deletes the thread. Worktree removal runs only on user cancel (Cause.hasInterruptsOnly). AGitCommandErrorafter the claim leaves~/.t3/worktrees/<repo>/t3code-<id>and brancht3code/<id>.git worktree removealso does not delete the branch, so even cancel can leave the ref.The RPC
vcs.createWorktreepath has no bootstrap cleanup at all, so a driver-level fix is the right place.Trigger vs. bug
The common real-world lock is Claude Code’s Linux/WSL sandbox deny-writing
<main-repo>/.git/config.lockfor every sandboxed Bash command, including from another thread’s worktree (anthropics/claude-code#78818). That is an upstream aggravator. T3 still should not fail thread create or leave orphans when a best-effort config write cannot take the lock. The new thread’s provider is irrelevant.Why this isn’t a duplicate
- Git command errors discard stderr, making failures opaque to callers #4380 / open fix(server): name the cause of a failed git command #8645: opaque git errors. A
reasontag would name the lock; it would not keep the worktree or clean the orphan. fix(server): name the cause of a failed git command #8645 still does not put raw stderr on the error (deliberate). - [Bug]: New worktree creation fails when git fetch exceeds the hardcoded 30s timeout #10916: 30s
git fetchtimeout before add. - [Bug]: OrbStack systemd service misses SSH_AUTH_SOCK, causing worktree creation to fail at git fetch #10179: missing
SSH_AUTH_SOCKon fetch. - Deleting an archived thread silently orphans its git worktree #9085 / open fix(web): clean up worktrees when deleting archived threads #9116: orphans when deleting an archived thread.
- Deleting multiple threads asks about each orphaned worktree in a separate dialog #7067: multi-dialog orphan UI.
- Worktree creation fails when path's are too long #635: path-too-long on add.
- Merged feat(web): show each worktree setup step and let users cancel it #11372: added the cancel-only rollback that this failure path skips.
No open PR fixes #11735.
Suggested fix
- Treat
configureBaseRefas best-effort, same as submodule checkout: optional short retry for a transient lock, then log a warning and return the worktree. - If any post-add step stays required, roll back with
git worktree remove --forceand delete the new branch so a failed call is a no-op. - In
ws.ts, remove a claimed worktree on any bootstrap failure, not only cancel.
Workaround remains: delete a stale 0-byte
0444.git/config.lock, create the worktree while no other thread is in Bash, thengit worktree remove <path> && git branch -D t3code/<id>.Triage: confirmed bug on
apps/server. Not a duplicate. Accepting for a server fix.- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 14, 2026
Steps to reproduce
Deterministic, without any AI provider involved:
touch <repo>/.git/config.lock(any pre-existing file works; git's config writer usesO_CREAT|O_EXCLon that path).How it actually happens in practice (no manual
touch): Claude Code's Linux/WSL sandbox deny-writes<main-repo>/.git/config.lockwith a bubblewrap bind mount and materializes a 0-byte, mode 0444 placeholder on the host for the duration of every sandboxed Bash command, in the main repo's.giteven when the thread runs in a linked worktree (anthropics/claude-code#78818, confirmed by a maintainer). Any T3 worktree creation that overlaps a Bash command in another thread hits step 2 for free, and the provider chosen for the new thread is irrelevant: my last failure was a Codex thread, created while a Claude thread in another worktree was 5 minutes into a 7-minute test run. With three busy threads I got 7 failures in one afternoon.Expected behavior
Either of:
createWorktreetreatsgit config branch.<new>.gh-merge-base <base>as best-effort: log a warning, keep the worktree, let the thread start (the merge base can be re-derived later,resolveBaseBranchForNoUpstreamalready falls back to the default branch).git worktree addand the branch so no orphan is left behind, and retry the config write a few times (the lock is transient by nature).Also: surface git's stderr in the error (#4380). "Git command exited with a non-zero status." hid a one-line
could not lock config file .git/config: File existsthat would have made the diagnosis trivial.Actual behavior
GitVcsDriver.createWorktreerunsgit worktree add -b <new> <path> <ref>(succeeds), thengit config branch.<new>.gh-merge-base <base>in the main repo, which exits 255. The whole command fails with:The thread is never created, but the worktree directory under
~/.t3/worktrees/<repo>/t3code-<id>and the brancht3code/<id>remain. I had to clean up 4 orphans by hand (git worktree remove+git branch -D); nothing in the UI shows them.How bad is this in practice?
Major degradation or frequent failure
Version or commit
T3 Code (Alpha) 0.0.40, WSL runtime
sha256-02c38816d0b6722af4ec74c48982ab009586fe7f84274cc757fe8f039c4784dbEnvironment
Windows 11 desktop app, WSL2 Ubuntu (kernel 6.18.33.2-microsoft-standard-WSL2), git 2.53.0, Claude Code 2.1.270 as provider with the sandbox enabled
Logs or stack traces
(The last span reads
SuccessbecauseexecuteGitruns withallowNonZeroExit: trueand checks the exit code outside the span, so the trace never records the failing exit code or stderr.)Reproduced by hand on the host while the placeholder existed:
Workaround
Delete the stale
<repo>/.git/config.lockwhen it is a 0-byte 0444 file (a bubblewrap placeholder, not a git lock), create the worktree while no other thread is running a Bash command, and remove orphans withgit worktree remove <path> && git branch -D t3code/<id>.