Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/devkit-scrub-git-hook-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@xnetjs/devkit': patch
---

`NodeCommandRunner` now scrubs git repo-location env vars (`GIT_DIR`,
`GIT_WORK_TREE`, `GIT_INDEX_FILE`, `GIT_OBJECT_DIRECTORY`,
`GIT_ALTERNATE_OBJECT_DIRECTORIES`, `GIT_COMMON_DIR`, `GIT_PREFIX`) from the
inherited environment before spawning subprocesses. Previously, running devkit
inside a git hook (where `git commit` exports `GIT_INDEX_FILE`) silently
redirected every spawned `git` at the hook's repository instead of the caller's
`cwd`, breaking temp-repo workflows and worktree creation with errors like
".git/index: index file open failed: Not a directory". An explicit value passed
via `options.env` still wins.
5 changes: 4 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,7 @@ fi

pnpm lint-staged
pnpm turbo run typecheck --affected
pnpm vitest run --changed HEAD --passWithNoTests
# Scrub the repo-location env vars `git commit` exports into hooks: tests that
# create their own temp repos (e.g. packages/devkit) would otherwise have their
# spawned git processes redirected at THIS repo's index and fail.
env -u GIT_INDEX_FILE -u GIT_DIR -u GIT_WORK_TREE pnpm vitest run --changed HEAD --passWithNoTests
21 changes: 21 additions & 0 deletions packages/devkit/src/command-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,25 @@ describe('NodeCommandRunner (real subprocess)', () => {
expect(r.ok).toBe(false)
expect(r.code).toBe(-1)
})

it('scrubs inherited git repo-location vars but honours explicit env overrides', async () => {
// Simulate running inside a git hook (`git commit` exports GIT_INDEX_FILE).
process.env.GIT_INDEX_FILE = '/parent/repo/.git/index'
process.env.GIT_DIR = '/parent/repo/.git'
try {
const print =
"process.stdout.write(String(process.env.GIT_INDEX_FILE ?? '') + '|' + String(process.env.GIT_DIR ?? ''))"
const scrubbed = await runner.run('node', ['-e', print], { cwd: process.cwd() })
expect(scrubbed.stdout).toBe('|')

const overridden = await runner.run('node', ['-e', print], {
cwd: process.cwd(),
env: { GIT_INDEX_FILE: '/explicit/index' }
})
expect(overridden.stdout).toBe('/explicit/index|')
} finally {
delete process.env.GIT_INDEX_FILE
delete process.env.GIT_DIR
}
})
})
28 changes: 27 additions & 1 deletion packages/devkit/src/command-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@

import { spawn } from 'node:child_process'

/**
* Repo-location env vars that `git` exports into hook subprocesses (e.g.
* `git commit` sets GIT_INDEX_FILE for pre-commit hooks). If a devkit process
* runs inside a hook and inherits these, every spawned `git` is silently
* redirected at the hook's repo instead of the `cwd` the caller asked for —
* temp-repo tests and agent worktrees break with errors like
* ".git/index: index file open failed: Not a directory". The runner drops them
* from the inherited env; a caller that really wants one can still set it
* explicitly via `options.env`.
*/
const GIT_REPO_LOCATION_VARS = [
'GIT_DIR',
'GIT_WORK_TREE',
'GIT_INDEX_FILE',
'GIT_OBJECT_DIRECTORY',
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
'GIT_COMMON_DIR',
'GIT_PREFIX'
] as const

function sanitizedEnv(): NodeJS.ProcessEnv {
const env = { ...process.env }
for (const key of GIT_REPO_LOCATION_VARS) delete env[key]
return env
}

export interface CommandResult {
/** `true` when the process exited 0. */
ok: boolean
Expand Down Expand Up @@ -40,7 +66,7 @@ export class NodeCommandRunner implements CommandRunner {
return new Promise((resolve) => {
const child = spawn(command, args, {
cwd: options.cwd,
env: { ...process.env, ...options.env },
env: { ...sanitizedEnv(), ...options.env },
shell: false // never interpret the command through a shell
})
let stdout = ''
Expand Down
Loading