From 37370e1e7dd8353433ab50bf76a0fb316be4311e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 18 Apr 2026 21:17:26 +0200 Subject: [PATCH] fix(cli): set GIT_CLONE_PROTECTION_ACTIVE=0 for skills (GH #316) (#328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #316 — `hyperframes skills` (and `npx skills add heygen-com/hyperframes`) fails with: ``` ■ Failed to clone repository fatal: active \`post-checkout\` hook found during \`git clone\` └ Installation failed ``` ## Root cause Two layers stacked: 1. **Git 2.45+ refuses to execute hooks during `git clone` by default.** The opt-in is `GIT_CLONE_PROTECTION_ACTIVE=0` — the env-var name is intentionally explicit about the trade-off. 2. **Users who ran `git lfs install` globally have a post-checkout hook registered at `core.hooksPath`.** When the upstream `skills` CLI shells out to `git clone` to fetch a repo's `skills/` directory, git detects the user's LFS hook and aborts. The check fires for **any repo**, regardless of whether the cloned repo uses LFS itself — it's protection against the user's own hooks, not the repo's content. Users who have git-lfs installed (very common) hit this for every clone the `skills` CLI does. ## The fix `hyperframes skills` wraps `npx skills add`. The wrapper now sets `GIT_CLONE_PROTECTION_ACTIVE=0` on the spawned child's env via a single helper (`gitCloneFriendlyEnv`) with a docstring at the call site explaining exactly why. The rest of `process.env` is preserved — proxy settings, extra CA certs, locale, etc. stay untouched. ## What this fix doesn't do (deliberately) This is the **code-path-we-own** fix. The deeper root cause is that the upstream `skills` CLI (vercel-labs/skills) should set this env var when it shells out to `git clone`. That would fix the bug for every user invoking `skills` directly — not just those who route through our wrapper. An upstream issue should be opened separately; not landing it as part of this PR. ## Users who call `npx skills add` directly Documented in the new troubleshooting subsection: set the env var manually. ```bash GIT_CLONE_PROTECTION_ACTIVE=0 npx skills add heygen-com/hyperframes ``` ## Tests `packages/cli/src/commands/skills.test.ts` — 2 cases: - Every spawned child has `GIT_CLONE_PROTECTION_ACTIVE=0` - The rest of `process.env` is preserved (not a wiped env) Uses `vi.mock` on `node:child_process` because ESM doesn't allow live-module `vi.spyOn` on re-exported bindings. ## Docs `docs/packages/cli.mdx` — new **Troubleshooting** subsection under the `skills` command. Explains both the automatic fix (`hyperframes skills` users are already covered) and the manual workaround (`npx skills add …` users). ## Closes - #316 --- docs/packages/cli.mdx | 20 ++++++++++ packages/cli/src/commands/skills.test.ts | 49 ++++++++++++++++++++++++ packages/cli/src/commands/skills.ts | 8 ++++ 3 files changed, 77 insertions(+) create mode 100644 packages/cli/src/commands/skills.test.ts diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index 4af280e57..d64f66a57 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -630,6 +630,26 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ | `--cursor` | Install to Cursor (`.cursor/skills/` in current project) | Skills are fetched from GitHub and include composition authoring, GSAP animation patterns, registry block/component wiring, and other domain-specific knowledge. The `init` command also offers to install skills automatically after scaffolding a project. + + #### Troubleshooting: `fatal: active post-checkout hook found during git clone` + + If you installed Git LFS globally (`git lfs install`), Git 2.45+ refuses to run the LFS post-checkout hook during any `git clone` — including the clone the upstream `skills` CLI performs under the hood. The error looks like: + + ``` + ■ Failed to clone repository + fatal: active `post-checkout` hook found during `git clone` + └ Installation failed + ``` + + **Using `hyperframes skills` is already fine** — as of v0.4.5 the CLI sets `GIT_CLONE_PROTECTION_ACTIVE=0` on the child environment, which is the opt-in knob Git provides for exactly this case. You don't need to do anything. + + **If you ran `npx skills add heygen-com/hyperframes` directly** (bypassing the HyperFrames CLI), set the env var yourself: + + ```bash + GIT_CLONE_PROTECTION_ACTIVE=0 npx skills add heygen-com/hyperframes + ``` + + This is tracked in [GH #316](https://github.com/heygen-com/hyperframes/issues/316). An upstream fix in the `skills` CLI itself is the right long-term answer; until that lands, the env var is the correct workaround. diff --git a/packages/cli/src/commands/skills.test.ts b/packages/cli/src/commands/skills.test.ts new file mode 100644 index 000000000..a0219a5bb --- /dev/null +++ b/packages/cli/src/commands/skills.test.ts @@ -0,0 +1,49 @@ +// ESM forbids `vi.spyOn` on live module exports, so we mock +// `node:child_process` at the loader level and inspect the spawned +// child's env. + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { EventEmitter } from "node:events"; + +type SpawnCall = { + command: string; + args: ReadonlyArray; + env: NodeJS.ProcessEnv | undefined; +}; + +const state: { calls: SpawnCall[] } = { calls: [] }; + +vi.mock("node:child_process", () => ({ + execFileSync: vi.fn(() => Buffer.from("11.0.0")), + spawn: vi.fn( + (command: string, args: ReadonlyArray, opts?: { env?: NodeJS.ProcessEnv }) => { + state.calls.push({ command, args, env: opts?.env }); + const fake = new EventEmitter(); + setImmediate(() => fake.emit("close", 0, null)); + return fake; + }, + ), +})); + +describe("hyperframes skills", () => { + beforeEach(() => { + state.calls = []; + vi.resetModules(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("sets GIT_CLONE_PROTECTION_ACTIVE=0 on the spawned skills CLI child (GH #316)", async () => { + const { default: skillsCmd } = await import("./skills.js"); + await skillsCmd.run?.({ args: {}, rawArgs: [], cmd: skillsCmd } as never); + + const first = state.calls[0]; + expect(first).toBeDefined(); + expect(first!.command).toBe("npx"); + expect(first!.args).toContain("skills"); + expect(first!.args).toContain("add"); + expect(first!.env?.GIT_CLONE_PROTECTION_ACTIVE).toBe("0"); + }); +}); diff --git a/packages/cli/src/commands/skills.ts b/packages/cli/src/commands/skills.ts index e0433c075..ebf24046d 100644 --- a/packages/cli/src/commands/skills.ts +++ b/packages/cli/src/commands/skills.ts @@ -17,6 +17,14 @@ function runSkillsAdd(repo: string): Promise { const child = spawn("npx", ["skills", "add", repo, "--all"], { stdio: "inherit", timeout: 120_000, + // GH #316 — the upstream `skills` CLI shells out to `git clone`. + // When Git's clone-hook protection is active (shipped on by + // default in 2.45.1, reverted in 2.45.2, still present on many + // corporate and CI setups), any globally-registered + // `git lfs install` post-checkout hook aborts the clone. The + // `repo` reaching this function is hardcoded in SOURCES below + // — no user input reaches the spawn — so opting out here is safe. + env: { ...process.env, GIT_CLONE_PROTECTION_ACTIVE: "0" }, }); child.on("close", (code, signal) => { if (code === 0) resolve();