homesponsors ⭐
 
   
🔍

Git hooks with Husky

July 12, 2026

Git hooks run scripts around local Git events (pre-commit, commit-msg, 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.

Husky wires those hooks through Git's core.hooksPath. Hook files live in .husky/ as plain shell scripts, and a prepare script installs them after npm install so every clone gets the same setup.

This post covers Husky v9 setup, common hooks, lint-staged, skipping hooks in CI, pitfalls, and a runnable demo that blocks messy commits.

Prerequisites

  • Node.js version 26
  • A Git repository (git init if you are starting fresh)
  • npm i -D husky
  • Optional but recommended: npm i -D lint-staged plus ESLint and Prettier

Setup

Install Husky and initialize it:

npm i -D husky
npx husky init

husky init creates .husky/pre-commit and adds a prepare script to package.json:

{
"scripts": {
"prepare": "husky"
}
}

prepare runs after local npm install, so teammates get hooks without a separate setup step. Replace the default pre-commit body with the checks you want.

Mental model

PieceRole
prepare / huskyPoints Git at .husky/ via core.hooksPath
.husky/<hook>Shell script Git runs for that event
lint-stagedRuns tools only on staged files (typical pre-commit target)
CIStill runs full lint/test; hooks are local only

Husky does not invent a new hook system - it configures native Git hooks and keeps the scripts in the repo.

Pre-commit with lint-staged

Running eslint . or prettier --write on the whole tree from pre-commit gets slow. lint-staged limits work to staged files.

npm i -D lint-staged eslint prettier
// package.json
{
"lint-staged": {
"*.{js,ts}": ["eslint --fix", "prettier --write"]
}
}
# .husky/pre-commit
npx lint-staged

On git commit, Git runs .husky/pre-commitlint-staged → ESLint/Prettier on matching staged paths. Auto-fixed files are restaged. If a check exits non-zero, the commit is aborted.

Keep pre-commit fast and file-local. Put slow or whole-repo work in pre-push or CI.

Other useful hooks

commit-msg - validate the message file Git passes as $1:

# .husky/commit-msg
message=$(cat "$1")
if echo "$message" | grep -qE '^(WIP|wip)($|[^a-zA-Z])'; then
echo "WIP commits are blocked by the commit-msg hook"
exit 1
fi

pre-push - run a quicker subset of CI before a push:

# .husky/pre-push
npm test

Create a hook by adding an executable script under .husky/ with the Git hook name. Prefer POSIX shell for Windows compatibility.

Skipping hooks

Skip once with Git's no-verify flag:

git commit -m "emergency fix" -n

Disable Husky for a command or environment:

HUSKY=0 git commit -m "skip hooks"

Set HUSKY=0 (or HUSKY: 0 in CI env) on CI/Docker so installs do not try to configure local hooks. If production installs omit devDependencies, use a safe prepare script so missing husky does not fail the install:

{
"scripts": {
"prepare": "husky || true"
}
}

Pitfalls

  • Hooks are local - they do not run on the remote; CI still matters.
  • --no-verify / HUSKY=0 - anyone can bypass hooks; do not rely on them for security.
  • GUI + Node version managers - GUI clients often lack the shell PATH where nvm/fnm put Node. Source your version manager from ~/.config/husky/init.sh.
  • Nested package vs Git root - Husky will not install into a parent ../ by default. In a monorepo, install Husky at the repo root (or adjust prepare as in the Husky how-to).
  • Slow pre-commit - full test suites belong in pre-push or CI; keep commit hooks on staged files.

When to use what

ApproachGood for
Husky + lint-stagedFast lint/format on staged files before commit
Husky commit-msgLightweight message conventions locally
Husky pre-pushShort tests before sharing a branch
CI onlyFull lint, build, and test on every change

Pair Husky with ESLint and Prettier for the usual Node.js quality gate on commit.

Git SSH keys setup

June 19, 2026

SSH keys let you authenticate with Git hosts without entering a password on every push or pull. This post covers generating keys for multiple providers, loading them into the SSH agent, and cloning repositories over SSH.

For general Git commands, see the Git cheatsheet.

Generate SSH keys

Generate a separate key for each provider (for example, GitHub and Bitbucket). Run ssh-keygen for each key, set a distinct filename, and optionally add a passphrase.

ssh-keygen -f ~/.ssh/id_rsa_github
ssh-keygen -f ~/.ssh/id_rsa_bitbucket

Add public keys to providers

Copy each public key (the .pub file) and add it to the provider:

cat ~/.ssh/id_rsa_github.pub
cat ~/.ssh/id_rsa_bitbucket.pub

Activate SSH keys

Start the SSH agent and add the keys you need for the current session.

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa_github
ssh-add ~/.ssh/id_rsa_bitbucket

Verify the connection to a provider:

ssh -T git@github.com
ssh -T git@bitbucket.org

Shell aliases per provider

Add aliases to ~/.bashrc or ~/.zshrc to run the agent setup and load the correct key in one command.

alias ssh-github='eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_rsa_github'
alias ssh-bitbucket='eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_rsa_bitbucket'

Reload the shell configuration, then run the alias before working with that provider:

source ~/.zshrc
ssh-github

Clone repositories

Use the SSH remote URL when cloning. The host (github.com or bitbucket.org) determines which key the provider expects.

# git clone <REPOSITORY_URL>
git clone git@github.com:workspace/repo-name.git
git clone git@bitbucket.org:workspace/repo-name.git

If authentication fails, run the matching alias (for example, ssh-github) and try again.

2023

Git cheatsheet

January 6, 2023

Git is one of the version control systems, and it's a prerequisite for development jobs. This post covers most of the git commands I use.

Configuration

  • Set user configuration for every project if you use multiple accounts
    git config user.name "<USERNAME>"
    git config user.email "<EMAIL_ADDRESS>"
  • Use the current branch for push commands
    git config --global push.default current
  • Set up SSH keys for GitHub, Bitbucket, and other providers - see Git SSH keys setup

Basic commands

  • The repository setup
    • Initialize a git repository
      git init
    • Clone an existing repository
      # git clone <REPOSITORY_URL>
      git clone git@github.com:zsevic/pwa-starter.git
    • Add the remote repository
      # git remote add <REMOTE_NAME> <REPOSITORY_URL>
      git remote add origin git@github.com:zsevic/pwa-starter.git
      git remote add upstream git@github.com:zsevic/pwa-starter.git
    • Update the URL for the remote repository
      # git remote set-url <REMOTE_NAME> <REPOSITORY_URL>
      git remote set-url origin git@github.com:zsevic/pwa-starter.git
    • Get a list of configured remote connections
      git remote -v
  • Branches
    • Get a list of the branches
      git branch
    • Create and switch to the new branch
      git checkout -b new-branch
    • Checkout to a specific commit and create a new branch out of it
      git log # find a hash from a specific commit
      git checkout <COMMIT_HASH>
      git switch -c <NEW_BRANCH_NAME>
    • Switch to another branch
      git checkout existing-branch
    • Rename the current branch
      git branch -m <NEW_BRANCH_NAME>
    • Delete branch
      git branch -D other-existing-branch
    • Fetch all the remote branches
      git fetch --all
    • Get a list of remote branches without cloning the repo or verify if the user has "read" access
      git ls-remote <REPOSITORY_URL>
  • Get the status of the local changes
    git status
  • Add new changes
    git add some-file.js
    git add .
  • Commits
    • Commit the changes
      git commit -m "Commit message"
    • Empty commit without any files
      git commit --allow-empty -m "Trigger CI pipeline"
    • Commit the changes and skip running git hooks
      git commit -m "Commit message" --no-verify
    • Update the latest commit message and add new changes to the latest commit
      git commit -m "Commit message" --amend
  • Push the changes to the remote repository
    • Push the changes to the current branch when the current branch is configured as the default one
      git push
    • Push the changes to the remote branch
      # git push <REMOTE_NAME> <BRANCH_NAME>
      git push origin master
    • Force push the changes to the feature branch
      # git push <REMOTE_NAME> <FEATURE_BRANCH_NAME>
      git push origin feature-branch -f
  • Fetch and merge remote changes to the local branch
    # git pull <REMOTE_NAME> <BRANCH_NAME>
    git pull origin master
  • Remove (unstage) the changes from the local stage
    git reset some-file.js
    git reset
  • Differences between commits
    • Get a difference compared to the latest commit
      git diff some-file.js
      git diff
    • Get a difference between the last two commits
      git diff HEAD^ HEAD
      # or
      git diff HEAD HEAD~1
  • Revert the file changes
    git checkout -- some-file.js
  • Merge the specified branch into the current one
    git merge <BRANCH_NAME>
  • Revert specific commit. The following command creates a new commit
    git revert <COMMIT_HASH>

Miscellaneous

  • Resets

    • Soft reset (commits are removed, but changes from the removed commits are staged)
      # git reset --soft HEAD~{NUMBER_OF_COMMITS_TO_SOFT_REMOVE}
      git reset --soft HEAD~2
    • Hard reset (both commits and changes are removed)
      # git reset --hard HEAD~{NUMBER_OF_COMMITS_TO_HARD_REMOVE}
      git reset --hard HEAD~1 # equal as git reset --hard HEAD^
    • Get the latest remote changes when pulling doesn't work
      git reset --hard origin/<BRANCH_NAME>
  • Stashing

    git add .
    git stash save <STASH_NAME>
    git stash list
    git stash apply --index 0
  • Tags

    • Remove the following tag locally
      git tag -d v0.13.29
  • Find removed commits

    git reflog
    git checkout <COMMIT_HASH>
  • Remove the initial commit

    git update-ref -d HEAD
  • Patching

    • Create a patch from the latest commits
      # git format-patch -{NUMBER_OF_COMMITS}
      git format-patch -1
    • Apply the patches
      git apply 0001-latest-commit.patch
  • Git submodules

    • Add git submodule
      # git submodule add -- <REPOSITORY_URL> <DIRECTORY_PATH>
      git submodule add -- git@github.com:zsevic/pwa-starter.git template
    • Retrieve the latest changes for the git submodule
      # git submodule update --remote <DIRECTORY_PATH>
      git submodule update --remote template
    • Resolve conflict in the submodule
      # git reset HEAD <DIRECTORY_PATH>
      git reset HEAD template
      git commit