{"componentChunkName":"component---src-gatsby-theme-chronoblog-templates-note-js","path":"/notes/husky-hooks/","result":{"data":{"mdx":{"parent":{"__typename":"File","fields":{"gitLogLatestDate":"2026-07-12 15:52:49 +0200"}},"id":"de4886f5-b16a-5a8d-a688-bda214fcac12","excerpt":"Git hooks run scripts around local Git events ( pre-commit ,  commit-msg ,  pre-push , and others). They are a good place for fast checks…","frontmatter":{"title":"Git hooks with Husky","date":"2026-07-12 15:45:00 UTC","job_ad":null,"job_ad_id":null,"job_ad_url":null,"tags":["git","husky","node","lint-staged","eslint","prettier"],"cover":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAA6klEQVQoz52Si26DMAxF8/+/uXWDPggJSciTMwWE2jLoqlmyEtm+9zqxxTRNvOPV1nO5L77GVxf/IbzH+VUneMNeiz3XCtUnjCmEEIkxYk1Gq0yKNebx3v8lh3MZ1WdimBCfH5rbNaCUxgwDvQxcL5UoIqWk625oHWfA6PIiMsZZtJcR5yJae87tSPCbJ9f2lUrUrmuyWikZ2XnaxjHoKixRynA5j3ydBoxJOFvJF4y4f/pEzsyFbWOxNpFzopQy547/t9BLT/NtGV15nDKHwL1pPk7/aSgroJK9Am7X58jF/n7tL/Ne11vcD57HFVpRCDakAAAAAElFTkSuQmCC","aspectRatio":2.0869565217391304,"src":"/static/82d09cb5e55878cedc5c91b98154d228/c4ecb/cover.png","srcSet":"/static/82d09cb5e55878cedc5c91b98154d228/57ab0/cover.png 192w,\n/static/82d09cb5e55878cedc5c91b98154d228/f4739/cover.png 384w,\n/static/82d09cb5e55878cedc5c91b98154d228/c4ecb/cover.png 768w","srcWebp":"/static/82d09cb5e55878cedc5c91b98154d228/dd090/cover.webp","srcSetWebp":"/static/82d09cb5e55878cedc5c91b98154d228/ae504/cover.webp 192w,\n/static/82d09cb5e55878cedc5c91b98154d228/fef30/cover.webp 384w,\n/static/82d09cb5e55878cedc5c91b98154d228/dd090/cover.webp 768w","sizes":"(max-width: 768px) 100vw, 768px","presentationWidth":768,"presentationHeight":366},"resize":{"src":"/static/82d09cb5e55878cedc5c91b98154d228/c4ecb/cover.png"}}}},"fields":{"slug":"/notes/husky-hooks/","readingTime":{"text":"4 min read"}},"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Git hooks with Husky\",\n  \"date\": \"2026-07-12 15:45:00 UTC\",\n  \"cover\": \"./cover.png\",\n  \"tags\": [\"git\", \"husky\", \"node\", \"lint-staged\", \"eslint\", \"prettier\"],\n  \"canonical_url\": \"https://sevic.dev/notes/husky-hooks/\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"Git hooks run scripts around local Git events (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"pre-commit\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"commit-msg\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"pre-push\"), \", and others). They are a good place for fast checks - lint, format, and lightweight validation - before bad changes leave your machine. Hooks are not a substitute for CI; treat them as an early filter.\"), mdx(\"p\", null, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://typicode.github.io/husky/\"\n  }), \"Husky\"), \" wires those hooks through Git's \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"core.hooksPath\"), \". Hook files live in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".husky/\"), \" as plain shell scripts, and a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"prepare\"), \" script installs them after \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"npm install\"), \" so every clone gets the same setup.\"), mdx(\"p\", null, \"This post covers Husky v9 setup, common hooks, \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"lint-staged\"), \", skipping hooks in CI, pitfalls, and a runnable demo that blocks messy commits.\"), mdx(\"h3\", {\n    \"id\": \"prerequisites\"\n  }, \"Prerequisites\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Node.js version 26\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"A Git repository (\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"git init\"), \" if you are starting fresh)\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"npm i -D husky\")), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Optional but recommended: \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"npm i -D lint-staged\"), \" plus \", mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/linting-eslint/\"\n  }), \"ESLint\"), \" and \", mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/formatting-prettier/\"\n  }), \"Prettier\"))), mdx(\"h3\", {\n    \"id\": \"setup\"\n  }, \"Setup\"), mdx(\"p\", null, \"Install Husky and initialize it:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"npm i -D husky\\nnpx husky init\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"husky init\"), \" creates \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".husky/pre-commit\"), \" and adds a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"prepare\"), \" script to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"package.json\"), \":\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-json\"\n  }), \"{\\n  \\\"scripts\\\": {\\n    \\\"prepare\\\": \\\"husky\\\"\\n  }\\n}\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"prepare\"), \" runs after local \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"npm install\"), \", so teammates get hooks without a separate setup step. Replace the default \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"pre-commit\"), \" body with the checks you want.\"), mdx(\"h3\", {\n    \"id\": \"mental-model\"\n  }, \"Mental model\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Piece\"), mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Role\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"prepare\"), \" / \", mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"husky\"))), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Points Git at \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \".husky/\"), \" via \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"core.hooksPath\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \".husky/<hook>\"))), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Shell script Git runs for that event\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"lint-staged\"))), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Runs tools only on staged files (typical \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"pre-commit\"), \" target)\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"CI\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Still runs full lint/test; hooks are local only\")))), mdx(\"p\", null, \"Husky does not invent a new hook system - it configures native Git hooks and keeps the scripts in the repo.\"), mdx(\"h3\", {\n    \"id\": \"pre-commit-with-lint-staged\"\n  }, \"Pre-commit with lint-staged\"), mdx(\"p\", null, \"Running \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"eslint .\"), \" or \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"prettier --write\"), \" on the whole tree from \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"pre-commit\"), \" gets slow. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"lint-staged\"), \" limits work to staged files.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"npm i -D lint-staged eslint prettier\\n\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-json\"\n  }), \"// package.json\\n{\\n  \\\"lint-staged\\\": {\\n    \\\"*.{js,ts}\\\": [\\\"eslint --fix\\\", \\\"prettier --write\\\"]\\n  }\\n}\\n\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-sh\"\n  }), \"# .husky/pre-commit\\nnpx lint-staged\\n\")), mdx(\"p\", null, \"On \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"git commit\"), \", Git runs \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".husky/pre-commit\"), \" \\u2192 \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"lint-staged\"), \" \\u2192 ESLint/Prettier on matching staged paths. Auto-fixed files are restaged. If a check exits non-zero, the commit is aborted.\"), mdx(\"p\", null, \"Keep \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"pre-commit\"), \" fast and file-local. Put slow or whole-repo work in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"pre-push\"), \" or CI.\"), mdx(\"h3\", {\n    \"id\": \"other-useful-hooks\"\n  }, \"Other useful hooks\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"commit-msg\")), \" - validate the message file Git passes as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"$1\"), \":\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-sh\"\n  }), \"# .husky/commit-msg\\nmessage=$(cat \\\"$1\\\")\\n\\nif echo \\\"$message\\\" | grep -qE '^(WIP|wip)($|[^a-zA-Z])'; then\\n  echo \\\"WIP commits are blocked by the commit-msg hook\\\"\\n  exit 1\\nfi\\n\")), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"pre-push\")), \" - run a quicker subset of CI before a push:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-sh\"\n  }), \"# .husky/pre-push\\nnpm test\\n\")), mdx(\"p\", null, \"Create a hook by adding an executable script under \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".husky/\"), \" with the Git hook name. Prefer POSIX shell for Windows compatibility.\"), mdx(\"h3\", {\n    \"id\": \"skipping-hooks\"\n  }, \"Skipping hooks\"), mdx(\"p\", null, \"Skip once with Git's no-verify flag:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"git commit -m \\\"emergency fix\\\" -n\\n\")), mdx(\"p\", null, \"Disable Husky for a command or environment:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"HUSKY=0 git commit -m \\\"skip hooks\\\"\\n\")), mdx(\"p\", null, \"Set \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"HUSKY=0\"), \" (or \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"HUSKY: 0\"), \" in CI env) on CI/Docker so installs do not try to configure local hooks. If production installs omit \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"devDependencies\"), \", use a safe prepare script so missing \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"husky\"), \" does not fail the install:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-json\"\n  }), \"{\\n  \\\"scripts\\\": {\\n    \\\"prepare\\\": \\\"husky || true\\\"\\n  }\\n}\\n\")), mdx(\"h3\", {\n    \"id\": \"pitfalls\"\n  }, \"Pitfalls\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Hooks are local\"), \" - they do not run on the remote; CI still matters.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"--no-verify\"), \" / \", mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"HUSKY=0\")), \" - anyone can bypass hooks; do not rely on them for security.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"GUI + Node version managers\"), \" - GUI clients often lack the shell \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"PATH\"), \" where \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"nvm\"), \"/\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"fnm\"), \" put Node. Source your version manager from \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"~/.config/husky/init.sh\"), \".\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Nested package vs Git root\"), \" - Husky will not install into a parent \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"../\"), \" by default. In a monorepo, install Husky at the repo root (or adjust \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"prepare\"), \" as in the \", mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://typicode.github.io/husky/how-to.html\"\n  }), \"Husky how-to\"), \").\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Slow pre-commit\"), \" - full test suites belong in \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"pre-push\"), \" or CI; keep commit hooks on staged files.\")), mdx(\"h3\", {\n    \"id\": \"when-to-use-what\"\n  }, \"When to use what\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Approach\"), mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Good for\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"Husky + lint-staged\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Fast lint/format on staged files before commit\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"Husky \", mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"commit-msg\"))), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Lightweight message conventions locally\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"Husky \", mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"pre-push\"))), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Short tests before sharing a branch\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"CI only\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Full lint, build, and test on every change\")))), mdx(\"p\", null, \"Pair Husky with \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/linting-eslint/\"\n  }), \"ESLint\"), \" and \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/formatting-prettier/\"\n  }), \"Prettier\"), \" for the usual Node.js quality gate on commit.\"));\n}\n;\nMDXContent.isMDXComponent = true;"}},"pageContext":{"id":"de4886f5-b16a-5a8d-a688-bda214fcac12"}},"staticQueryHashes":["1961101537","2542493696"]}