Skip to content

[Bug]: Checkpoint capture fails with exit 128 on every turn when the project path is a symlink to a repository subdirectory #13187

Description

@m2moiz

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

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

  1. 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.)
  2. Add the symlink path as a project in T3 Code.
  3. Start a thread and let one turn finish.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions