---
name: setup-husky-pre-commit
source: https://app.decimal.ai/s/setup-husky-pre-commit@1/SKILL.md
source_sha256: 527512f796bd
---

# Husky v9 + lint-staged + Prettier house config

## Contract

Enforces the exact, arbitrary house style for a Husky v9 + lint-staged + Prettier
pre-commit setup. Apply whenever the task is to add pre-commit hooks, initialize Husky,
configure lint-staged, or wire commit-time formatting / typecheck / test into a Node repo.
Produce these EXACT files. Do not improvise alternatives, do not modernize, do not "improve."

## Rules (the complete spec)

### R1 — `.husky/pre-commit` is Husky v9 style (no boilerplate)

- **NO shebang line.** `#!/bin/sh`, `#!/usr/bin/env sh`, or any `#!` first line is FORBIDDEN.
  Husky v9 sources the hook itself; a shebang is v8-and-earlier residue.
- **NO source line.** `. "$(dirname "$0")/_/husky.sh"` (and the older
  `. "$(dirname -- "$0")/_/husky.sh"`) is FORBIDDEN. That is v8 boilerplate.
- The file is **just the commands** — nothing else, no comments required.

### R2 — the hook runs exactly three commands, in THIS order

1. `npx lint-staged`
2. `npm run typecheck`
3. `npm run test`

Order is load-bearing: format/lint the staged files first (fast, staged-only), then the
slower full-repo `typecheck`, then `test`. Never reorder, never collapse into one line.

### R3 — adapt the runner to the detected package manager

- Detect from the lockfile: `package-lock.json` → npm, `pnpm-lock.yaml` → pnpm,
  `yarn.lock` → yarn, `bun.lockb` (or `bun.lock`) → bun.
- Replace `npm`/`npx` accordingly:
  - npm: `npx lint-staged`, `npm run typecheck`, `npm run test`
  - pnpm: `pnpm lint-staged` (or `pnpm exec lint-staged`), `pnpm run typecheck`, `pnpm run test`
  - yarn: `yarn lint-staged`, `yarn typecheck`, `yarn test`
  - bun: `bunx lint-staged`, `bun run typecheck`, `bun run test`
- **No lockfile present → DEFAULT TO npm.** Do not ask, do not guess pnpm/yarn.

### R4 — only wire scripts that exist

- If `package.json` has no `typecheck` script, OMIT line 2 and tell the user it was omitted.
- If it has no `test` script, OMIT line 3 and tell the user.
- **Never invent** a `typecheck`/`test` script or a stub command (no `tsc --noEmit`, no
  `echo "no tests"`). lint-staged (line 1) is the only always-present line.

### R5 — `.lintstagedrc` is exactly the all-files Prettier rule

```json
{
  "*": "prettier --ignore-unknown --write"
}
```

- The glob key is **`"*"`** — a single asterisk matching ALL files. NOT `"*.{js,ts}"`,
  NOT `"**/*"`, NOT a per-extension map.
- The command is **`prettier --ignore-unknown --write`**. `--ignore-unknown` is REQUIRED
  (Prettier silently skips files it can't parse — images, lockfiles — instead of crashing
  the commit). `--write` formats in place.
- **No ESLint** in this config. Prettier only.

### R6 — `.prettierrc` only if no Prettier config already exists

Create it **only** when the repo has no Prettier config (no `.prettierrc`, `.prettierrc.json`,
`.prettierrc.js`, `prettier.config.js`, or a `prettier` key in `package.json`). If one exists,
leave it untouched. When you do create it, use EXACTLY these seven keys/values:

```json
{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": false,
  "trailingComma": "es5",
  "semi": true,
  "arrowParens": "always"
}
```

The arbitrary, easy-to-get-wrong values: `trailingComma` is **`"es5"`** (NOT `"all"`),
`singleQuote` is **`false`** (double quotes), `semi` is **`true`**, `arrowParens` is
**`"always"`**, `printWidth` is **`80`**, `tabWidth` is **`2`**, `useTabs` is **`false`**.

### R7 — package.json changes

- Add to **devDependencies**: `husky`, `lint-staged`, `prettier` (these three, no more).
- Set the `prepare` script to **`"husky"`** — NOT `"husky install"` (the v8 form).
- Initialize with **`npx husky init`** (creates `.husky/`, adds the `prepare` script). NOT
  the legacy `npx husky install` / `husky add`.

### R8 — the commit

Stage everything and commit with EXACTLY this message:

```
Add pre-commit hooks (husky + lint-staged + prettier)
```

Verbatim — same wording, same parenthical, lower-case `husky + lint-staged + prettier`.
This commit also smoke-tests the hook (it runs through the new pipeline).

## Worked examples (BEFORE = base default → AFTER = conforming)

### R1 — no v8 boilerplate in the hook

BEFORE (base emits the v8-style hook it was trained on):
```sh
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged
```
AFTER (v9 — no shebang, no source line, three commands):
```
npx lint-staged
npm run typecheck
npm run test
```

### R2 — three commands, correct order

BEFORE (base collapses to one line, or reorders tests first):
```
npm test && npx lint-staged
```
AFTER:
```
npx lint-staged
npm run typecheck
npm run test
```

### R3 — pnpm runner

BEFORE (base hardcodes npm even though `pnpm-lock.yaml` is present):
```
npx lint-staged
npm run typecheck
npm run test
```
AFTER (pnpm detected):
```
pnpm exec lint-staged
pnpm run typecheck
pnpm run test
```

### R4 — repo has only a `build` script

BEFORE (base invents the missing scripts):
```
npx lint-staged
npm run typecheck
npm run test
```
AFTER (omit the two missing lines, and say so):
```
npx lint-staged
```
> Note: your package.json has no `typecheck` or `test` script, so those lines were omitted.
> Add the scripts and re-add the lines if you want them gated at commit time.

### R5 — the lint-staged glob

BEFORE (base scopes to JS/TS extensions, its common default):
```json
{
  "*.{js,ts,jsx,tsx}": "prettier --write"
}
```
AFTER (all files, ignore-unknown):
```json
{
  "*": "prettier --ignore-unknown --write"
}
```

### R6 — the Prettier config

BEFORE (base reaches for the "modern" defaults — `all` commas, single quotes):
```json
{
  "trailingComma": "all",
  "singleQuote": true,
  "tabWidth": 2
}
```
AFTER (house style — es5 commas, double quotes, full seven keys):
```json
{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": false,
  "trailingComma": "es5",
  "semi": true,
  "arrowParens": "always"
}
```

### R7 — the prepare script

BEFORE (base uses the deprecated v8 form):
```json
{ "scripts": { "prepare": "husky install" } }
```
AFTER:
```json
{ "scripts": { "prepare": "husky" } }
```

### R8 — the commit message

BEFORE: `chore: add husky and prettier pre-commit hooks`
AFTER: `Add pre-commit hooks (husky + lint-staged + prettier)`

## Edge cases & exceptions

- **No lockfile at all** → npm (R3). Don't infer from the presence of a `pnpm-workspace.yaml`
  or a `.yarnrc`; only the four lockfiles decide, and absence means npm.
- **Multiple lockfiles** → prefer the one matching the repo's `packageManager` field if set;
  otherwise npm. Don't emit two runners.
- **Existing Prettier config** → never overwrite or recreate it (R6). Still emit `.lintstagedrc`
  and the hook. Mention you kept their config.
- **Existing `.husky/pre-commit`** → replace its contents with the v9 three-line form; do not
  append your lines under their old shebang/source boilerplate.
- **`typecheck` exists but not `test`** (or vice-versa) → keep the one that exists, omit and
  report the missing one. It is not all-or-nothing.
- **Monorepo / workspaces** → still a single root `.husky/pre-commit` and root `.lintstagedrc`;
  do not scatter per-package hooks unless explicitly asked.
- **ESLint already in the repo** → still do NOT add eslint to `.lintstagedrc`; this config is
  Prettier-only by house rule. Mention they can add lint to the `typecheck`/`test` scripts.

## Do / Don't

- Never add a shebang to `.husky/pre-commit`; always emit the bare v9 three-line file.
- Never add the `_/husky.sh` source line; always omit it (v9).
- Never use the glob `"*.{js,ts}"`; always use `"*"`.
- Never drop `--ignore-unknown`; always include it so unparseable files don't crash the commit.
- Never set `trailingComma` to `"all"`; always `"es5"`.
- Never set `singleQuote` to `true`; always `false`.
- Never set `prepare` to `"husky install"`; always `"husky"`.
- Never invent a `typecheck`/`test` script; always omit the line and tell the user.
- Never reword the commit message; always exactly `Add pre-commit hooks (husky + lint-staged + prettier)`.

## Common mistakes (the base's wrong defaults)

- Emitting the v8 hook (`#!/usr/bin/env sh` + `. .../_/husky.sh`) from muscle memory.
- Scoping lint-staged to `*.{js,ts,jsx,tsx}` instead of `"*"`, and dropping `--ignore-unknown`.
- Choosing the "Prettier 3 modern" defaults (`trailingComma: "all"`, `singleQuote: true`).
- Using `"husky install"` for the prepare script.
- Hardcoding npm even when a pnpm/yarn/bun lockfile is present.
- Auto-generating `typecheck`/`test` scripts (e.g. `tsc --noEmit`, `echo "no tests"`) instead
  of omitting the missing lines.
- Adding ESLint into the lint-staged map.
- Rewriting the commit message into a Conventional-Commit form (`chore: …`).

## Quick checklist

- [ ] `.husky/pre-commit`: no shebang, no source line, `lint-staged` → `typecheck` → `test`.
- [ ] runner matches the lockfile (npm if none).
- [ ] missing `typecheck`/`test` lines omitted + reported.
- [ ] `.lintstagedrc` = `{"*": "prettier --ignore-unknown --write"}`.
- [ ] `.prettierrc` only if absent; the exact seven keys, `es5` commas, double quotes.
- [ ] devDeps: husky, lint-staged, prettier; `prepare: "husky"`; init via `npx husky init`.
- [ ] commit: `Add pre-commit hooks (husky + lint-staged + prettier)`.
