Skip to content

Git lookup on PATH - #56

Merged
llima merged 16 commits into
mainfrom
fix/git-lookup-on-path
Oct 13, 2026
Merged

llima merged 16 commits into
mainfrom
fix/git-lookup-on-path

Conversation

@llima

@llima llima commented Oct 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Version bumped to 0.27.2
  • Merging publishes craftar@0.27.2 to npm after approval in the npm environment
  • Security fix. craftar runs in other people's repositories; git was started by name, so the platform's lookup could pick a git sitting in the directory craftar runs in (a workspace or a Forge) through an empty or a relative PATH entry. git is now resolved by craftar itself, over the absolute entries of PATH only — the lookup dotnet already had — and every git call goes through one runner in src/core/proc.ts (onPath, execProgram).
  • The program craftar starts (git; dotnet for templates install and doctor's probe) gets a PATH of absolute entries only, so what it starts in turn (ssh, a credential helper) is not looked for in the working directory either.
  • Behaviour change, in README › Upgrading › to 0.27.2: a git reachable only through an empty or relative entry, or with PATH unset, is no longer found — craftar then behaves as on a machine without git (doctor says git not found; a remote Forge fails; the git-gated Forge writers refuse; and for a path Forge the next writing sync rewrites craftar.lock once with forge.commit: null, the registry recording the same). status --json's forge.commit is null in that case — a value the contract already allows. No generated file changes.
  • A test scans src/ so that src/core/proc.ts stays the only importer of node:child_process, the two docker call sites aside.

Known and out of this PR:

  • npx craftar puts <cwd>/node_modules/.bin (and every ancestor's) first on PATH as absolute entries, so a git committed there still runs, as before. The README says so; closing it needs a design of its own.
  • docker is still started by name (craftar services, doctor's docker check) — unchanged here.
  • Windows: the win32 arm of the child's PATH (childEnv) and the win32-only tests in test/proc.test.ts were never run before this PR — its CI is their first run.

Notes on the history (never amended):

  • 7827e97 added git-config isolation to test/helpers/remote.ts and 3c1bd70 took it out again (its body says why: dropping the configuration on Windows was not measured). The helper is exactly what main has; the ssh test isolates its own runs.

Test plan

  • npm run typecheck — pass (on the bumped HEAD e91913e)
  • npm run build — pass
  • npx vitest run --exclude test/ci.test.ts --maxWorkers=6 — 92 files passed, 1 skipped; 2485 tests passed, 8 skipped (Linux). Oracle skipped (no fixture): no emitter is in the range; the one lock change (forge.commit: null) is pinned by test/git-lookup.test.ts. test/ci.test.ts is left to CI.
  • node bin/craftar.js --version prints 0.27.2
  • A git planted in a workspace or a Forge does not run; an ssh planted there is not what git starts (with a positive control) — test/git-lookup.test.ts, Linux
  • CI on Windows — first run of the two win32-only tests (PATH read whatever its key's case and a .cmd is not a program; the child's PATH set and empty)
  • CI green on Linux
  • After merge: approve the release run only when ready to publish craftar@0.27.2

llima added 16 commits October 12, 2026 20:25
Committed failing on purpose: with an empty or relative PATH entry the platform runs it (measured on Linux); the next commit routes every git call through onPath.
…t and stderr

Adds execProgram for running external programs with both stdout and stderr returned.
The program is resolved by onPath, never by the platform's lookup. Rewrites capture
as one line over it, and updates notOnPath to match Node's error shape.
…om craftar's working directory

Every git call now goes through execProgram, which resolves the program by onPath
before running it. A git planted in a workspace or a Forge — directories that come
from a clone — is never executed.
…s only

Documents that the same PATH lookup used for dotnet also applies to git:
absolute entries only, never the working directory.
…or dotnet starts is not looked for in the working directory either

Git starts ssh for ssh URLs, credential helpers, hooks. The lookup craftar does for
the main program now also applies to the PATH it hands the child.
…ge.commit, once

Pins the one lock byte this fix can move. Existing behavior, now documented.
… forge.commit — and what the child's PATH holds

Documents the lock behavior pinned by the previous commit and the child PATH security fix.
… the PATH key is replaced

An empty PATH is itself a working-directory lookup: with it, git started an ssh planted beside it
(review, measured). An absent PATH makes the platform use its default path. Off Windows, Path and
path are other variables and are passed as given.
…once; git in tests ignores the developer's config

Review: with GIT_SSH_COMMAND or a global core.sshCommand the ssh test passed whatever the code did.
It now clears them and carries a positive control; the lock test compares bytes for "once".
…s no longer found either

The child's PATH holds absolute entries only, which also hides a credential helper, a
git-remote-* or git-lfs reachable only through a relative entry (review).
… test is for the other platforms

Review: libuv gives a Windows child with no PATH key the parent's own, so "absent means the default
path" holds off Windows only, and the test would fail there. The git test helper keeps the system
and global configuration it had: dropping them on Windows was not measured.
…DME says where it was measured

Review: nothing failed without the win32 arm of childEnv. The test is written from Node's and
libuv's sources and has not run until a Windows job runs it; the README scopes the claim about
what git starts in turn to where it was measured.
…s not cover

After the rebase onto local services, docker is started by name in
src/cli.ts and src/core/services.ts. The scan lists them as a known gap
and checks that neither starts git or dotnet by name.
…he npx case, the registry and what else stops without git

Review: the rebase left the two bullets under "to 0.26.0", a published version, and rewrote its "No byte change"
line. They move to a section of their own (its number is settled at the version bump), 0.26.0's line is main's
again, and the text now says what it left out: PATH entries npx adds are absolute, the registry records the null
commit too, the gated Forge writers refuse, and the child's PATH is filtered for dotnet as well.
…ft as it was (review)

Review round 2: the forge-moved mark goes silent because of the commit git no longer gives, not because of the
registry; doctor's dotnet probe gets the filtered PATH as templates install does, and docker does not; a unify
run with --save-plan passes no git gate; and what was reasoned, not measured, says so.
@llima
llima merged commit 9976956 into main Oct 13, 2026
14 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant