Skip to content

chore: refuse commits in the primary checkout - #599

Merged
theCodeDrift merged 1 commit into
mainfrom
chore/refuse-main-checkout
Nov 2, 2026
Merged

theCodeDrift merged 1 commit into
mainfrom
chore/refuse-main-checkout

Conversation

@theCodeDrift

Copy link
Copy Markdown
Member

Agents kept ending up in the main checkout, editing and committing in the user's own working tree. Instructions alone did not stop it, so a pre-commit guard now refuses the commit.

  • .agents/skills/worktrees-pnpm/scripts/refuse-main-checkout.sh refuses a commit when git rev-parse --git-dir equals --git-common-dir, which is true only in the primary checkout, never in a linked worktree. It checks the checkout, not the branch, so a feature branch checked out in the main tree is refused too.
  • CI is exempt (CI set): the changeset release and Claude review workflows commit from a plain clone, which is a primary checkout.
  • git commit --no-verify is the deliberate override. AGENTS.md says an agent never uses it to get past this guard.
  • .husky/pre-commit runs the guard before lint-staged.
  • The skill's Setup section documents it as step 3, so a repo that copies the skill gets the guard with it.
  • AGENTS.md also tells agents not to offer to fast-forward the local main: worktrees branch from a freshly fetched origin/main, so the local branch is never read.

Verified in a scratch repo: refused in the primary checkout; commits with --no-verify, with CI=true, in a worktree, and from a worktree subdirectory. This commit was itself made from a worktree through the new hook.

No changeset: repo tooling, nothing ships.

A pre-commit guard refuses any commit made in the repository's primary
checkout, so all work happens in a linked worktree under
.agents/.worktrees/. It compares --git-dir with --git-common-dir, which
are equal only in the primary checkout, so it refuses a feature branch
checked out there too. CI is exempt, since runners commit from a plain
clone. git commit --no-verify is the deliberate override.

The guard ships in the worktrees-pnpm skill so other repos get it with
the skill. AGENTS.md also stops agents offering to fast-forward the
local main, which nothing here reads.
@theCodeDrift

Copy link
Copy Markdown
Member Author

@claude /review

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @theCodeDrift's task in 3m 34s —— View job


Review: chore: refuse commits in the primary checkout

Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json has no prior review threads, no prior review summaries, and only the triggering @claude /review comment, so this is the first review of this PR — everything below is new.

Finding: the guard doesn't actually block the commit

.husky/pre-commit (husky ^9.1.7, per package.json) is a plain sh script with no set -e:

sh .agents/skills/worktrees-pnpm/scripts/refuse-main-checkout.sh
npx lint-staged

Without set -e or && chaining, a non-zero exit from the first line does not stop the script — npx lint-staged still runs, and the hook's final exit status is lint-staged's, not the guard's. So in the primary checkout: the guard prints its refusal to stderr and exits 1, the shell continues to lint-staged, lint-staged succeeds, the hook exits 0, and git completes the commit. The guard is bypassed with no --no-verify needed — confirmed by inspecting the sibling .husky/post-merge hook in this repo, which relies on the same no-implicit-errexit behavior and uses explicit || instead of relying on early exit.

The PR description says this was "verified in a scratch repo: refused in the primary checkout," so either the manual test exercised something other than the full husky git commit flow (e.g. running the guard script directly), or lint-staged happened to fail for an unrelated reason during that test and masked the problem. Either way, as written this will not reliably refuse commits.

The identical pattern is also reproduced in the new SKILL.md section (the sh .../refuse-main-checkout.sh / npx lint-staged code block), so any repo that copies the documented setup verbatim inherits the same gap.

Fix is small — chain with &&, or add set -e to .husky/pre-commit:

sh .agents/skills/worktrees-pnpm/scripts/refuse-main-checkout.sh && npx lint-staged

Left inline comments on both spots.

Everything else

  • refuse-main-checkout.sh itself is sound: --git-dir vs --git-common-dir is the right test for "primary checkout vs. linked worktree," it's branch-agnostic as intended, the CI exemption is reasonable, and the --no-verify escape hatch is clearly documented. No functional issues inside the script.
  • AGENTS.md changes (the --no-verify guidance and the "don't offer to fast-forward local main" note) are documentation-only and consistent with the rest of the PR — no issues.
  • No submodules in this repo (no .gitmodules), so the --git-dir/--git-common-dir equality check won't false-positive on a submodule checkout here; not worth guarding against speculatively.

Comment thread .husky/pre-commit Resolved
Comment thread .agents/skills/worktrees-pnpm/SKILL.md Resolved
@theCodeDrift

Copy link
Copy Markdown
Member Author

Re: @github-actions — "Claude finished @theCodeDrift's task in 3m 34s … Finding: the guard doesn't actually block the commit"
#599 (comment)

Not a bug: Husky 9 runs .husky/pre-commit through .husky/_/h as sh -e, so the guard's non-zero exit ends the hook before lint-staged. Re-tested through the real Husky runner in a scratch repo: refused, lint-staged stub never ran, nothing committed. No change made.

— AI Coding Agent

@theCodeDrift
theCodeDrift merged commit 9e5d64e into main Nov 2, 2026
10 checks passed
@theCodeDrift
theCodeDrift deleted the chore/refuse-main-checkout branch November 2, 2026 01:27
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