Before submitting
Checked and ruled out: #9245 (symlinked worktree paths compared as strings, different code), #12204 (git dir followed when the back link is missing, different trigger), #5489 (intermittent exit 128 during checkpoint restore, caused by an existing index.lock), and #12602 (open PR that calls realpath in the new metadata path but does not touch the checkpoint code below).
Area
apps/server
Steps to reproduce
In the app
- Have a git repository with a subdirectory, and a symlink somewhere else that points at that subdirectory. (My case: I keep several tool-managed folders as symlinks into subdirectories of one repository, and opened one of those symlinks as a project.)
- Add the symlink path as a project in T3 Code.
- Start a thread and let one turn finish.
- The turn ends with
VCS process failed in GitVcsDriver.checkpoints.captureCheckpoint: git (<project path>) exited with 128. It repeats after every turn, and the thread never gets a checkpoint.
The mechanism, without the app
This uses Node's path.resolve, the same call resolveGitCommonDir makes, so it isolates the bug in a few lines:
R=$(mktemp -d)
git init -q "$R/repo"
git -C "$R/repo" -c user.name=x -c user.email=x@example.invalid commit -q --allow-empty -m init
mkdir -p "$R/repo/sub/dir"
ln -s "$R/repo/sub/dir" "$R/link"
cd "$R/link"
rel=$(git rev-parse --git-common-dir) # ../../.git, relative to git's PHYSICAL cwd
res=$(node -e 'console.log(require("path").resolve(process.argv[1], process.argv[2]))' "$R/link" "$rel")
echo "$res" # the symlink's grandparent + /.git: does not exist
GIT_INDEX_FILE="$res/t3-checkpoint-index-test" git read-tree HEAD; echo "exit=$?" # exit=128
git rev-parse --path-format=absolute --git-common-dir # the correct directory
Observed, one row per way of opening the same repository (paths shortened):
| Project path |
--git-common-dir |
path.resolve(projectPath, it) |
read-tree into that dir |
| symlink to a repo subdirectory |
../../.git |
/tmp/.git (does not exist) |
exit 128 |
| symlink to the repo root |
.git |
<link>/.git (resolves through the link) |
exit 0 |
| the real subdirectory path |
../../.git |
<repo>/.git |
exit 0 |
It takes both: a symlink in the project path, and a project folder that is a subdirectory of the repository, so the relative answer contains ...
Expected behavior
A project opened through a symlink behaves like the directory it points to: checkpoints are captured each turn and "Go back" is available.
Actual behavior
Every turn fails checkpoint capture with exit 128, and the thread has no checkpoints. The agent itself keeps working, because nothing else in the turn depends on the checkpoint.
Cause, in apps/server/src/vcs/GitVcsDriver.ts at main @ 6975efd3dd52, lines 756 to 765:
const resolveGitCommonDir = (cwd: string) =>
Effect.gen(function* () {
const result = yield* execute({ /* ... */ cwd, args: ["rev-parse", "--git-common-dir"] });
const gitCommonDir = result.stdout.trim();
return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir);
});
Git computes the relative path from its physical working directory, but path.resolve applies the .. segments lexically to the project path string. When that string goes through a symlink to a subdirectory, the .. climbs out of the symlink's parent instead of the repository. captureCheckpoint then builds tempIndexPath inside that nonexistent directory, and the first git command given it as GIT_INDEX_FILE fails:
fatal: Unable to create '<wrong dir>/.git/t3-checkpoint-index-<uuid>.lock': No such file or directory
From reading the source (not observed separately): the index-reuse branch fails first, at fileSystem.copyFile(..., tempIndexPath), and is swallowed by Effect.orElseSucceed(() => false). The fallback read-tree is what surfaces as exit 128. Git's actual message above is not shown anywhere, which is the stderr-discarding problem described in #12224 and #4380.
Suggested fix (one line): ask git for the absolute path, the same way this function already does for --git-path index at line 831:
args: ["rev-parse", "--path-format=absolute", "--git-common-dir"],
In the reproduction above, that form returns the correct directory from the symlinked path. realpath(cwd) before path.resolve would also work. --path-format needs git 2.31 or later, which line 831 already assumes.
Impact
Major degradation or frequent failure
For an affected project it fails on every turn, and checkpoints, diffs and "Go back" are all unavailable for the thread. It only affects projects opened through a symlink that points into a repository subdirectory.
Version or commit
T3 Code (Alpha) 0.0.42 desktop; the cause is present on main @ 6975efd3dd52 (2026-09-23)
Environment
macOS 27.0, git 2.55.0, Node v26.8.2, Claude provider
Logs or stack traces
# server.trace.ndjson, one failure per turn (home path redacted)
VcsProcessExitError: VCS process failed in GitVcsDriver.checkpoints.captureCheckpoint: git (<home>/<symlink to a repo subdirectory>) exited with 128 - Process exited with a non-zero status.
at VcsProcessExitError.fromProcessExit (app.asar/apps/server/dist/bin.mjs:31121:10)
at VcsProcess.runUnbounded (app.asar/apps/server/dist/bin.mjs:93005:47)
at VcsProcess.run (app.asar/apps/server/dist/bin.mjs:93073:72)
at GitVcsDriver.checkpoints.captureCheckpoint (app.asar/apps/server/dist/bin.mjs:93682:93)
at captureCheckpoint (app.asar/apps/server/dist/bin.mjs:93681:28)
Screenshots, recordings, or supporting files
No response
Workaround
Add the project by its real path (realpath <symlink>), or open the repository root instead of the symlinked subdirectory.
Before submitting
Checked and ruled out: #9245 (symlinked worktree paths compared as strings, different code), #12204 (git dir followed when the back link is missing, different trigger), #5489 (intermittent exit 128 during checkpoint restore, caused by an existing
index.lock), and #12602 (open PR that callsrealpathin the new metadata path but does not touch the checkpoint code below).Area
apps/server
Steps to reproduce
In the app
VCS process failed in GitVcsDriver.checkpoints.captureCheckpoint: git (<project path>) exited with 128. It repeats after every turn, and the thread never gets a checkpoint.The mechanism, without the app
This uses Node's
path.resolve, the same callresolveGitCommonDirmakes, so it isolates the bug in a few lines:Observed, one row per way of opening the same repository (paths shortened):
--git-common-dirpath.resolve(projectPath, it)read-treeinto that dir../../.git/tmp/.git(does not exist).git<link>/.git(resolves through the link)../../.git<repo>/.gitIt takes both: a symlink in the project path, and a project folder that is a subdirectory of the repository, so the relative answer contains
...Expected behavior
A project opened through a symlink behaves like the directory it points to: checkpoints are captured each turn and "Go back" is available.
Actual behavior
Every turn fails checkpoint capture with exit 128, and the thread has no checkpoints. The agent itself keeps working, because nothing else in the turn depends on the checkpoint.
Cause, in
apps/server/src/vcs/GitVcsDriver.tsatmain@6975efd3dd52, lines 756 to 765:Git computes the relative path from its physical working directory, but
path.resolveapplies the..segments lexically to the project path string. When that string goes through a symlink to a subdirectory, the..climbs out of the symlink's parent instead of the repository.captureCheckpointthen buildstempIndexPathinside that nonexistent directory, and the first git command given it asGIT_INDEX_FILEfails:From reading the source (not observed separately): the index-reuse branch fails first, at
fileSystem.copyFile(..., tempIndexPath), and is swallowed byEffect.orElseSucceed(() => false). The fallbackread-treeis what surfaces as exit 128. Git's actual message above is not shown anywhere, which is the stderr-discarding problem described in #12224 and #4380.Suggested fix (one line): ask git for the absolute path, the same way this function already does for
--git-path indexat line 831:In the reproduction above, that form returns the correct directory from the symlinked path.
realpath(cwd)beforepath.resolvewould also work.--path-formatneeds git 2.31 or later, which line 831 already assumes.Impact
Major degradation or frequent failure
For an affected project it fails on every turn, and checkpoints, diffs and "Go back" are all unavailable for the thread. It only affects projects opened through a symlink that points into a repository subdirectory.
Version or commit
T3 Code (Alpha) 0.0.42 desktop; the cause is present on
main@6975efd3dd52(2026-09-23)Environment
macOS 27.0, git 2.55.0, Node v26.8.2, Claude provider
Logs or stack traces
Screenshots, recordings, or supporting files
No response
Workaround
Add the project by its real path (
realpath <symlink>), or open the repository root instead of the symlinked subdirectory.