Skip to content

fix(devkit): isolate git subprocesses from inherited GIT_* env - #446

Merged
crs48 merged 1 commit into
mainfrom
claude/devkit-git-env-isolation
Jul 10, 2026
Merged

fix(devkit): isolate git subprocesses from inherited GIT_* env#446
crs48 merged 1 commit into
mainfrom
claude/devkit-git-env-isolation

Conversation

@crs48

@crs48 crs48 commented Jul 10, 2026

Copy link
Copy Markdown
Owner

What

@xnetjs/devkit's NodeCommandRunner now scrubs git's repo-location environment variables (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, and friends) for git invocations, so the explicit cwd is always authoritative. An explicit options.env entry still wins.

Why

RunOptions.cwd is required precisely so a caller "can never accidentally run in process.cwd()" — but git reads GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE from the environment over cwd. A husky hook (pre-commit/pre-push) exports exactly those vars pointing at the hook's repo. So when the pre-push hook runs pnpm test, devkit's real-repo tests (git.test.ts) spawned git children that inherited the hook's GIT_DIR and operated on the actual worktree instead of their temp repo — running git config, git commit, and even git push against it.

This was not hypothetical: it clobbered a live worktree and its remote branch during PR #445 (and recurred on #444) — the worktree's user.name/email got overwritten and junk initial/checkpoint commits were pushed to origin.

How

  • command-runner.ts: for command === 'git', merge { ...process.env, ...<git-location vars set to undefined>, ...options.env }. Node's spawn omits undefined-valued env keys, so the inherited leak is dropped while any explicit override is preserved. The scrubbed set is documented in the new GIT_LOCATION_ENV (module-internal — not added to the package barrel, so no public API change → patch).

Tests

  • command-runner.test.ts: with a leaked bogus GIT_DIR, git rev-parse --is-inside-work-tree still resolves the cwd repo (would be fatal without the scrub); and an explicit options.env GIT_DIR still wins.
  • git.test.ts: a checkpoint() lands in cwd while GIT_* points at a decoy, and the decoy never becomes a repo.
  • Ran the two devkit test files directly (13 passing). Did not run the full suite locally, to avoid re-triggering the very leak this fixes on an unpatched checkout.

🤖 Generated with Claude Code

Under a git hook (husky pre-push running `pnpm test`), the hook exports
GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE, which git honours over an explicit
cwd. So devkit's git subprocesses — including the real-repo tests in
git.test.ts — operated on the hook's repo instead of their temp worktree,
clobbering a live worktree and its remote (config, commit, and push all
misdirected; observed on PR #445 and again on #444).

NodeCommandRunner now scrubs git's repo-location env vars for `git`
invocations so cwd is always authoritative; an explicit options.env entry
still wins (spread last). Adds runner- and Git-level regression tests that
point GIT_* at a decoy and assert operations land in cwd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-446 July 10, 2026 14:32 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

✓ Changelog fragment found — thanks!

@crs48 crs48 added the skip-changelog Exclude this PR from the changelog label Jul 10, 2026
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #446.

github-actions Bot added a commit that referenced this pull request Jul 10, 2026
@crs48
crs48 merged commit c73bc27 into main Jul 10, 2026
13 of 14 checks passed
@crs48
crs48 deleted the claude/devkit-git-env-isolation branch July 10, 2026 14:44
github-actions Bot added a commit that referenced this pull request Jul 10, 2026
crs48 added a commit that referenced this pull request Jul 10, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @xnetjs/devkit@1.0.0

### Major Changes

- [#439](#439)
[`677856e`](677856e)
Thanks [@crs48](https://github.com/crs48)! - Secure the
browser↔local-model bridge (exploration 0289).
- **`@xnetjs/devkit` (breaking):** the agent bridge daemon now
**requires a
per-launch pairing token** (`Authorization: Bearer <token>`,
constant-time
compared) on its data endpoints (`/v1/chat/completions`, `/run`) and
validates
the `Host` header to reject DNS-rebinding requests. `BridgeServerConfig`
gains
`pairingToken?`, `BridgeServerHandle` exposes `pairingToken`, and a
token is
auto-generated when none is supplied — so a client that previously
called the
data endpoints with no auth now gets `401`. `/health` stays
unauthenticated so
detection still works before pairing. New `openAiChatAgent` lets the
bridge
front a raw OpenAI-compatible model server (Ollama/LM Studio) through
the same
    authenticated door.
- **`@xnetjs/plugins`:** `ConnectorEnv` gains `appOrigin` and the
local-server
setup hint now names the exact `OLLAMA_ORIGINS=<origin>` line (never a
wildcard); new `localServerSetupHint` export; the MCP HTTP transport now
validates the `Host` header (defense-in-depth, no change for legitimate
    callers). Additive.
- **`@xnetjs/cli`:** `xnet bridge serve` prints the pairing code and
gains
`--token` (pin the code) and `--upstream` / `--upstream-model` (front a
raw
    local model). Additive.

### Patch Changes

- [#446](#446)
[`10c9f87`](10c9f87)
Thanks [@crs48](https://github.com/crs48)! - Isolate git subprocesses
from inherited repo-location env. When the dev loop (or
its tests) ran while a git hook was active — e.g. husky `pre-push`
running
`pnpm test` — the hook's exported
`GIT_DIR`/`GIT_WORK_TREE`/`GIT_INDEX_FILE`
leaked into `git` children and overrode the explicit `cwd`, so
operations
(`config`, `commit`, even `push`) targeted the hook's repo instead of
the
requested worktree. `NodeCommandRunner` now scrubs git's repo-location
env vars
  for `git` invocations so `cwd` is always authoritative; an explicit
  `options.env` entry still wins.
## @xnetjs/cli@0.1.0

### Minor Changes

- [#439](#439)
[`677856e`](677856e)
Thanks [@crs48](https://github.com/crs48)! - Secure the
browser↔local-model bridge (exploration 0289).
- **`@xnetjs/devkit` (breaking):** the agent bridge daemon now
**requires a
per-launch pairing token** (`Authorization: Bearer <token>`,
constant-time
compared) on its data endpoints (`/v1/chat/completions`, `/run`) and
validates
the `Host` header to reject DNS-rebinding requests. `BridgeServerConfig`
gains
`pairingToken?`, `BridgeServerHandle` exposes `pairingToken`, and a
token is
auto-generated when none is supplied — so a client that previously
called the
data endpoints with no auth now gets `401`. `/health` stays
unauthenticated so
detection still works before pairing. New `openAiChatAgent` lets the
bridge
front a raw OpenAI-compatible model server (Ollama/LM Studio) through
the same
    authenticated door.
- **`@xnetjs/plugins`:** `ConnectorEnv` gains `appOrigin` and the
local-server
setup hint now names the exact `OLLAMA_ORIGINS=<origin>` line (never a
wildcard); new `localServerSetupHint` export; the MCP HTTP transport now
validates the `Host` header (defense-in-depth, no change for legitimate
    callers). Additive.
- **`@xnetjs/cli`:** `xnet bridge serve` prints the pairing code and
gains
`--token` (pin the code) and `--upstream` / `--upstream-model` (front a
raw
    local model). Additive.

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`853d849`](853d849),
[`10c9f87`](10c9f87),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/runtime@0.2.0
  - @xnetjs/devkit@1.0.0
  - @xnetjs/data@0.8.0
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/plugins@0.8.0

### Minor Changes

- [#420](#420)
[`dd3b1cb`](dd3b1cb)
Thanks [@crs48](https://github.com/crs48)! - Single-shell layout
primitives (exploration 0284): `createDefaultTree()` and
`DEFAULT_WORKSPACE_ID` join the workspace layout API — the one canonical
tree (a sectioned sidebar in the rail, the full left dock, tabs on) that
replaces the quiet/calm/bench preset trichotomy. Purely additive:
`createPresetTree` and the preset ids remain for the devtools seed and
portable-workspace round-trips.

- [#439](#439)
[`677856e`](677856e)
Thanks [@crs48](https://github.com/crs48)! - Secure the
browser↔local-model bridge (exploration 0289).
- **`@xnetjs/devkit` (breaking):** the agent bridge daemon now
**requires a
per-launch pairing token** (`Authorization: Bearer <token>`,
constant-time
compared) on its data endpoints (`/v1/chat/completions`, `/run`) and
validates
the `Host` header to reject DNS-rebinding requests. `BridgeServerConfig`
gains
`pairingToken?`, `BridgeServerHandle` exposes `pairingToken`, and a
token is
auto-generated when none is supplied — so a client that previously
called the
data endpoints with no auth now gets `401`. `/health` stays
unauthenticated so
detection still works before pairing. New `openAiChatAgent` lets the
bridge
front a raw OpenAI-compatible model server (Ollama/LM Studio) through
the same
    authenticated door.
- **`@xnetjs/plugins`:** `ConnectorEnv` gains `appOrigin` and the
local-server
setup hint now names the exact `OLLAMA_ORIGINS=<origin>` line (never a
wildcard); new `localServerSetupHint` export; the MCP HTTP transport now
validates the `Host` header (defense-in-depth, no change for legitimate
    callers). Additive.
- **`@xnetjs/cli`:** `xnet bridge serve` prints the pairing code and
gains
`--token` (pin the code) and `--upstream` / `--upstream-model` (front a
raw
    local model). Additive.

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/abuse@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/runtime@0.2.0

### Minor Changes

- [#448](#448)
[`853d849`](853d849)
Thanks [@crs48](https://github.com/crs48)! - `NodeStoreSyncProvider` now
handles hub capacity rejections gracefully: on the first
`QUOTA_EXCEEDED` (over the hub's per-user cap) or `STORAGE_FULL` (hub
disk full) rejection it pauses outbound sync instead of re-flooding the
hub, keeps local data intact, and resumes on the next reconnect.
Subscribe to the new `onSyncBlocked(listener)` API (with
`SyncBlockedReason`/`SyncBlockedListener` types) to surface a "storage
full" notice in your app.

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/history@0.8.0
  - @xnetjs/data-bridge@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/storage@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/abuse@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/crypto@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/core@0.8.0
## @xnetjs/data@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/storage@0.8.0
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/data-bridge@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/history@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/identity@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/react@0.8.0

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`853d849`](853d849),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/runtime@0.2.0
  - @xnetjs/history@0.8.0
  - @xnetjs/data-bridge@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/storage@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/sync@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/core@0.8.0


## @xnetjs/sqlite@0.8.0


## xnet-cloud@0.0.11

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.8.0
  - @xnetjs/cloud@0.0.1
## @xnetjs/brain@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/vectors@0.0.1
## @xnetjs/comms@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/dashboard@0.0.12

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/react@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/social@0.0.12
## @xnetjs/labs@0.0.12

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/data@0.8.0
## @xnetjs/licenses@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.8.0
## @xnetjs/maps@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
## @xnetjs/meetings@0.0.5

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/data@0.8.0
## @xnetjs/server@0.0.11

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data-bridge@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/social@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/unreal@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
## xnet-desktop@0.8.0

Desktop shell release riding the @xnetjs/core 0.8.0 train.
Desktop-specific changes are not tracked here; see the core packages'
changelogs for what shipped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Exclude this PR from the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant