fix(host-service): classify a repository with no work tree as a typed NOT_FOUND
Stacked on #6260 — base branch is sentry/git-env-classify, not main. Must merge after #6260.
Problem
HOST-SERVICE-Y is the highest-volume unaddressed issue in the org: 361 events lifetime, 45 in the last 24h, release 1.20.0, still new. Every event is Error: fatal: this operation must be run in a work tree on trpc_path: git.getStatus, reported with trpc_code: INTERNAL_SERVER_ERROR.
All stack frames belong to simple-git's executor chain (GitExecutorChain.attemptRemoteTask → handleTaskData → PluginStore.exec → the plugin that wraps stderr in a GitError); the first-party attribution comes from the tRPC path tag alone.
This is a user-environment condition, not a bug — the same class of condition the sibling patterns on this path already classify.
Root cause
#6260 added rethrowEnvironmentalGitError with three patterns: not a git repository, unable to read current working directory: no such file or directory, and unable to read current working directory. Git emits a different string when the directory resolves to a repository that has no work tree attached — the repo is bare, or a linked worktree's admin data was removed or pruned:
fatal: this operation must be run in a work tree
None of the three existing patterns match that text, so #6260 merging on its own would not have silenced this issue. The error falls through the guard, reaches the middleware as a 500, and reports.
Fix
One pattern constant, one branch, following the file's existing conventions exactly — a named module-level regex with a comment naming the git condition it represents, and a typed cause.kind:
// Git's text when the directory resolves to a repository but has no work tree
// attached: the repo is bare, or the linked worktree's admin data was removed
// or pruned. Distinct from CWD_GONE_PATTERN, where the directory itself is
// unlinked from under the running process.
const NOT_A_WORK_TREE_PATTERN = /this operation must be run in a work tree/i;
→ NOT_FOUND with cause: { kind: "NOT_A_WORK_TREE" }.
Why a new kind rather than reusing WORKTREE_MISSING: the HTTP code is the same, but the conditions are genuinely different and a consumer that wants to act on them needs to tell them apart. WORKTREE_MISSING fires when the process's own working directory has been unlinked out from under a running git — a race, the directory is gone. This one fires when the directory is present and readable and git resolves a repository there, but no work tree is attached. "Workspace directory was deleted, offer to recreate" and "this path is a bare repo or a pruned worktree entry" are not the same remediation, so they get separate kinds under the same NOT_FOUND.
We do not own the producer here: the text comes from the git CLI via simple-git, which exposes no errno or structured code on this path, so message matching is the only available signal. The regex is the distinctive phrase rather than an includes() of common words — a negative test pins that fatal: cannot chdir to work tree (which shares "work tree") does not match.
No existing pattern was broadened, and no behaviour changed beyond the classification: the guard, the message text, and the rethrow of unmatched errors are untouched.
What still reports as a 500 on the git status path
Everything that is not one of the four named environmental conditions. Specifically still INTERNAL_SERVER_ERROR and still reporting to Sentry:
index.lockcontention (fatal: Unable to create '.../index.lock': File exists)fatal: detected dubious ownership in repository- bad/ambiguous revisions and missing objects (
fatal: bad revision,fatal: bad object) - corrupt object database and ref-store failures
- worker-pool timeouts (the 15s
timeoutMson the status snapshot task) - any other unexpected simple-git or worker failure
The first three are pinned by negative tests so a future broadening of these patterns fails loudly.
Verification
Real output, all run on this branch.
bunx biome check on both changed files:
Checked 2 files in 9ms. No fixes applied.
cd packages/host-service && bun run typecheck:
$ tsc --noEmit --emitDeclarationOnly false
(clean — no diagnostics)
bun test packages/host-service/src/trpc/router/git:
bun test v1.3.14 (0d9b296a)
86 pass
0 fail
186 expect() calls
Ran 86 tests across 8 files. [26.07s]
The classifier file alone:
bun test v1.3.14 (0d9b296a)
5 pass
0 fail
16 expect() calls
Ran 5 tests across 1 file. [89.00ms]
Diffstat:
.../trpc/router/git/utils/classify-git-error.test.ts | 19 +++++++++++++++++++
.../src/trpc/router/git/utils/classify-git-error.ts | 12 ++++++++++++
2 files changed, 31 insertions(+)
Repo-wide bun run lint was not run (it hangs on cross-worktree bun locks); biome was run directly on the changed files instead.
Refs HOST-SERVICE-Y
https://claude.ai/code/session_408cd47d-49e8-41d4-b422-70b790efcfef
Summary by cubic
Classifies git.getStatus errors from repositories with no work tree as NOT_FOUND with cause.kind: "NOT_A_WORK_TREE" instead of INTERNAL_SERVER_ERROR. This reduces noisy Sentry reports and lets clients handle bare/pruned worktree conditions explicitly. Addresses HOST-SERVICE-Y.
- Adds
NOT_A_WORK_TREE_PATTERNand a branch inpackages/host-service/src/trpc/router/git/utils/classify-git-error.ts; preserves the original error message text. - Leaves other patterns and behavior unchanged; unmatched errors (e.g., index.lock contention, dubious ownership, bad revisions, timeouts) remain 500.
- Client action: handle TRPC
NOT_FOUNDwithcause.kind: "NOT_A_WORK_TREE"separately fromWORKTREE_MISSINGif remediation differs. - Extends tests to cover the new classification and negative cases to ensure non-matching errors are unchanged.