Install
$ agentstack add skill-bm629-agent-skills-polyglot-git-hooks ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
polyglot-git-hooks — SKILL.md
> Variant: standard · When to use: the skill is invoked, you produce a lefthook.yml and wire it, control returns to the caller. The tool taught is Lefthook (a Git-hooks manager); the skill is named for its differentiator — driving checks across multiple languages from one config.
Overview
This skill sets up Git hooks for a polyglot repo with Lefthook — a fast, Go-based, language-agnostic hooks manager. The goal is one lefthook.yml at the repo root that runs the right fast checks on staged files at pre-commit (format + lint) and the slower checks at pre-push (type-check + tests), across a mixed-language repo (e.g. a TypeScript subtree and a Python subtree), in parallel. It is the local "fail fast before the commit/push" layer that complements, not replaces, CI. This skill owns the hook wiring — the per-tool details (how Biome/Ruff/tsc/ty themselves work) belong to their own skills, which this one references.
When to activate
- ✅ Setting up Git hooks for a repo (a new
lefthook.yml), or adding apre-commit/pre-pushgate. - ✅ Wiring lint/format/type-check to run on staged files before a commit, or on push files before a push.
- ✅ Configuring hooks for a polyglot or monorepo where different subtrees use different toolchains.
- ✅ Migrating off husky+lint-staged or the Python
pre-commitframework to a single-config, polyglot setup.
Do NOT activate when:
- You need to learn or configure a specific linter/formatter/type-checker itself — use that tool's skill (
biome,ruff,ty,typescript-typecheck). - You are designing the CI pipeline (full, authoritative checks on the server) — hooks are the local gate; CI is separate (see Step 5 and the
turboreposkill). - The task is enforcing a
--no-verifyban as org policy — that is a narrow policy-hook concern, not hook setup (see Gotchas).
Workflow
Step 1: Pick Lefthook, know why
Lefthook is a single dependency-free binary (written in Go) that manages Git hooks for any language. Choose it over the alternatives for a polyglot repo because:
- vs husky + lint-staged — husky is JS/Node-only and needs
lint-stagedbolted on for staged-file filtering; the config ends up split across.husky/pre-commit, alint-stagedkey inpackage.json, andpackage.jsonscripts, and pulls a large dependency tree intonode_modules. Lefthook is onelefthook.yml, one binary, native staged-file filtering, no extra runtime deps. In a mixed frontend/backend team, husky+lint-staged tends to mean two separate setups. - vs the Python
pre-commitframework —pre-commitis Python-centric (it manages its own per-hook tool environments) and is weaker for TS/mixed repos. Lefthook runs commands in parallel, is a single compiled binary instead of a Python script, and works equally across languages and large monorepos. (pre-commitstill has a broader catalog of ready-made hooks and is simpler for small/Python-only projects — a fair trade to acknowledge, not the choice here.)
Step 2: Install + activate
Install Lefthook as a dev dependency or a binary, then wire it into .git/hooks with lefthook install.
- npm:
npm install lefthook --save-dev(also published as@evilmartians/lefthook). Thelefthooknpm package runs its ownpostinstallthat callslefthook install, so a freshnpm installwires the hooks automatically — the fresh-clone activation problem is solved for npm users out of the box. (See the fresh-clone note below.) - Binary / non-npm: install via Homebrew,
go install,pipx install lefthook,gem install lefthook, apt, snap, or winget, then runlefthook installonce.
Put a single lefthook.yml at the repo root. Verify the wiring with lefthook install (idempotent — safe to re-run).
Fresh-clone activation (load-bearing). Hooks live in each developer's local .git/hooks, which is not committed — so a teammate who just cloned the repo has no hooks until something runs lefthook install. Two ways to make a clone self-wire:
- npm projects: rely on the
lefthookpackage'spostinstall(automatic onnpm install), or add an explicit"prepare": "lefthook install"script topackage.jsonso it runs after install. (Forpnpm, addlefthooktopnpm.onlyBuiltDependencies/pnpm-workspace.yaml'sonlyBuiltDependencies, or the postinstall is skipped and hooks never wire.) - Non-npm projects: document
lefthook installas a required setup step in the README, or run it from a bootstrap/setup script.
In CI you usually do not want the hooks installed by the postinstall — set CI=true (most CI sets it already) to skip it; use LEFTHOOK=1 to force it on when needed.
Step 3: Author the lefthook.yml schema
Top-level keys are Git hook names — pre-commit, pre-push (any hook name works: commit-msg, post-merge, …). Under each hook, define work as either:
commands:— a map of named commands (the classic form), orjobs:— a list of jobs (the flexible form; supports commands, scripts, and nested groups with their ownparallel/pipedflow).glob/root/excludeset on a group apply to all nested jobs.
Use whichever reads cleaner; the examples here use commands:. Hook-level and command-level options you will reach for:
| Option | Where | What it does | |---|---|---| | run: | command | The shell command (runs via sh). Mandatory. | | glob: | command | Filter to matching files, e.g. "*.{ts,tsx}". A list of globs is allowed (Lefthook ≥ 1.10.10). If a file template is omitted from run, Lefthook still filters {staged_files} (pre-commit) / {push_files} (pre-push) by the glob and skips the command when nothing matches. | | root: | command | Change the command's working directory to a subtree (e.g. apps/web/). Useful when a package.json/config lives in a subdir. Globs are still computed from the git repo root — root does not change glob matching — but the file paths Lefthook substitutes into {staged_files} for the command are rebased to be relative to root, so the tool receives subtree-relative paths. | | exclude: | command | Drop files matching a regexp (or list), combined with glob. | | stage_fixed: | command | After the command runs, git add the (filtered) files so auto-fixes are re-staged. Works only for pre-commit. | | parallel: | hook | true → run the hook's commands concurrently. | | piped: | hook/group | true → run sequentially, stopping on the first failure. | | skip: / only: | hook or command | Conditions: true (always), merge / rebase / merge-commit, ref: main (glob refs like ref: dev/*), or run: (skip if the command exits 0). | | tags: / priority: | command | Group/order commands. |
File-template variables for run: — substituted at execution:
{staged_files}— files staged for the commit (populated atpre-commit).{push_files}— committed-but-unpushed files (populated atpre-push).{all_files}— all git-tracked files.{files}— the result of a customfiles:command.{cmd}— the command fromlefthook.yml(for wrapping, e.g. in Docker).
If a file list is too long for the OS command-line limit, Lefthook splits it and runs the command sequentially over the chunks.
Step 4: The polyglot worked example
A complete root lefthook.yml for a repo with a TypeScript subtree and a Python subtree. pre-commit runs fast, auto-fixing, staged-file checks in parallel, each scoped to its language; pre-push runs the slower type-check + test gates before code leaves the machine.
# lefthook.yml — at the repo root
min_version: 2.0.0 # pin the major version; warn on older Lefthook
pre-commit:
parallel: true
commands:
# --- TypeScript / JavaScript: format + lint staged files, re-stage fixes ---
biome:
glob: "*.{js,jsx,ts,tsx}"
run: biome check --write {staged_files}
stage_fixed: true
# --- Python: lint-fix + format staged files in the python subtree ---
ruff:
root: "services/api/" # CWD for the command (a python subtree)
glob: "*.py" # computed from the repo root, not from `root`
run: ruff check --fix {staged_files} && ruff format {staged_files}
stage_fixed: true
pre-push:
parallel: true
commands:
# --- TypeScript: whole-project type-check (no file template; checks the project) ---
tsc:
glob: "*.{ts,tsx}"
run: tsc --noEmit
# --- Python: type-check the subtree ---
ty:
root: "services/api/"
glob: "*.py"
run: ty check
# --- Tests: run the suites (heavier; pre-push, not pre-commit) ---
test-ts:
glob: "*.{ts,tsx}"
run: npm test
test-py:
root: "services/api/"
glob: "*.py"
run: pytest
Why this shape:
pre-commit= fast + staged-only.biome check --writeandruff check --fix/ruff formatonly touch{staged_files}, so the hook is fast and edits only what you are committing.stage_fixed: truere-stages the auto-fixes so the commit includes the formatted result (andstage_fixedis valid only here, atpre-commit).- Scope per subtree. Each command is bounded by
glob(language) and, for the Python side,root(the subtree's directory) so the right tool runs on the right files. Rememberglobmatches against repo-root-relative paths even whenrootis set. pre-push= slow + thorough. Type-checking (tsc --noEmit,ty check) and tests are project-wide and too slow to run on every commit, so they gate the push instead. Here the glob just decides whether the command runs (skip if no files of that language changed); the type-checkers and test runners then check the project, not just the changed files.{push_files}is available if you want to scope a push-time command to the changed set.- Parallel.
parallel: trueruns the language lanes concurrently — the TS and Python checks don't wait on each other.
The per-tool invocations (biome check, ruff check/ruff format, tsc --noEmit, ty check) are owned by their own skills — see ## Related. This file owns only the wiring.
Step 5: Place hooks against CI (don't duplicate)
Hooks and CI are different layers — wire them as a division of labor, not a copy:
- Hooks = local, staged, fast. They catch the obvious before the commit/push leaves your machine: formatting, lint, a quick type-check/test gate at push. Speed matters because they run on every commit; keep
pre-committo staged-file fast checks and push the slow stuff topre-push. - CI = full, authoritative. CI runs the complete suite on the server over the whole repo and is the enforced gate (a merge requirement) — because hooks are local and bypassable (Step 6), CI is what actually guarantees the checks ran.
Do not restate the CI pipeline inside lefthook.yml. Where a repo uses a task runner, a hook command may call a task (e.g. turbo run lint) rather than re-listing pipeline steps — but the pipeline definition itself lives with the task runner, not in the hook config. See the turborepo skill for the task graph; this skill does not duplicate it.
No-CI-yet caveat (important). When a repo has no CI pipeline yet, the hooks are the primary automated gate — there is no server-side net behind them. In that situation, raise the bar on pre-push: run the full type-check and test suites at push so nothing unverified leaves the machine, and treat the hook set as load-bearing until CI lands. Once CI exists, you can relax pre-push back toward a fast smoke gate and let CI own the exhaustive run.
Step 6: Document the --no-verify bypass
Hooks are local and can be skipped — make sure the team knows when that is legitimate and that it is not a silent gap:
git commit --no-verify(-n) andgit push --no-verifyskip Git hooks entirely.LEFTHOOK=0(orLEFTHOOK=false) before agitcommand disables Lefthook for that one invocation;LEFTHOOK_EXCLUDEoverrides theexclude_tagsconfig to drop specific tags/commands.
Legitimate uses: committing a known-incomplete WIP to a personal branch, or an emergency hotfix where the hook is blocking and CI will re-check anyway. Because the bypass exists, CI — not the hook — is the enforced gate (Step 5). Some teams add a policy hook that blocks --no-verify to make the bypass deliberate; a published example is wshobson/agents@block-no-verify-hook. That is a separate enforcement concern — named here as a pointer, not set up by this skill.
Rules
Hard rules (never violate):
- One
lefthook.ymlat the repo root. A single language-agnostic config is the whole point; don't fragment it per language. pre-commitis fast + staged-only. Run format/lint on{staged_files}withstage_fixed: true; never put whole-repo type-checks or full test suites atpre-commit.pre-pushcarries the slow gates. Type-check and tests go here, not atpre-commit.- Scope every command. Bound each command with
glob(androotfor a subtree) so the right tool runs on the right files. Don't run a Python tool over TS files or vice-versa. - This skill owns wiring, not tools. Reference
biome/ruff/ty/typescript-typecheckfor tool flags; referenceturborepofor the CI/task graph. Do not re-teach or duplicate them. - Pin the major version with
min_version:(current major: Lefthook 2.x) so a teammate on an older binary is warned rather than silently misbehaving.
Preferences (override-able):
- Prefer
commands:for simple per-language lanes; reach forjobs:+ groups when you need nestedpiped/parallelflow control. - Use
parallel: trueto run independent language lanes concurrently. - Add
skip: [merge, rebase]to commands that shouldn't run mid-merge/rebase.
Gotchas
- Fresh clone, no hooks. Hooks live in
.git/hooks, which is not committed — a teammate who clones gets nothing untillefthook installruns. Symptom: "the hook never ran for them." Fix: rely on the npmpostinstall(npm projects) or add apreparescript / documentlefthook install(Step 2). Forpnpm, the postinstall is skipped unlesslefthookis inonlyBuiltDependencies. rootdoes not move glob matching. Settingroot: "services/api/"changes the command's CWD butglobpatterns are still evaluated against repo-root-relative paths. A glob like"services/api/**/*.py"(full path) or a plain"*.py"(matches any depth) works; do not write the glob as if it were relative toroot.stage_fixedispre-commit-only. It is ignored atpre-pushand other hooks. If a formatter's fixes aren't being committed, confirm the command is underpre-commitand thatstage_fixed: trueis set on that command.- Empty match = skipped command (usually what you want). With a
globand no matching staged files, Lefthook skips the command rather than running it on zero files — so a TS-only commit won't trigger the Python lane. Don't add manual "if no files" guards; the glob already does it. - Stale config keys from old versions. Lefthook 2.x is current; very old configs/blog posts may use a deprecated key (e.g.
runner:instead ofrun:). Author against the current schema and letmin_version:flag a too-old binary. The Go module path is also.../lefthook/v2for v2. lefthook installand worktrees.lefthook installcan bake the install-time absolute path into the hook shim, which has been known to break git worktrees — re-runlefthook installin the worktree if hooks misfire there.- Hooks are not enforcement. Anyone can
--no-verify. Treat hooks as a fast local convenience and let CI be the gate that actually blocks a merge (Step 5/6).
Anti-patterns
- **"Put the
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: bm629
- Source: bm629/agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.