A tiny, Git-native hook runner driven by fishook.json. Installs stubs into .git/hooks and runs hook commands via bash -lc for any language/tooling.
npm install fishookGit hooks without a framework.
fishook is a tiny, transparent Git hook runner driven by a single file: fishook.json.
Just Git hooks β json config -> shell commands.
---
> Keep it simple, dummy.
* un-opinionated - Fishook is not opinionated, it just runs bash commands and helps you with a few shortcuts.
* simple - It is one or two steps simpler than modifying .git/hooks/, not twenty or thirty.
* framework agnostic - Fishook is written in pure bash and doesn't care if you use Node, Ruby, Python, Go, etc. git is git
* low-dependency - just uses bash, jq, git and sed.
---
* security - Prevent secrets from being committed
* quality - Run tests or lint before commit / push
* safety - Block direct pushes to protected branches
* standardization - Enforce commit message formats
* automation - Auto-generate commit messages or changelogs
* structure - Enforce file size, naming, or content rules
---
``bash
npm install --save-dev fishook # runs fishook install via postinstallor: pipx install fishook
or: pip install fishook
fishook install # install all hooks
$3
`json
{
"pre-commit": "npm test",
"post-checkout": "echo Checked out: $FISHOOK_REF"
}
`Thatβs it.
---
How it works
* Fishook installs lightweight Git hook shims
On each hook, it loads matching
fishook*.json files
* Commands are executed as-written using bash
* No background daemons, caches, or magic stateIf you can write a shell command, you can write a fishook rule.
---
$3
* connect to ANY hook git exposes
* use onFileEvent to run a command per file changed
* use applyTo and skipList to apply your command to only specific files
* use source to setup your environment before running commands
* use helpers whcih are already in the shell scope, like new, old, diff, modify to see file changes and update files in the staging area
* use raise to fail the hook with a message
* use multiple fishook.json files to allow commiting some to your repo while keeping others private---
Multiple config files (team + personal + scoped)
Fishook automatically loads all
fishook.json files in your repo (up to 4 levels deep), in alphabetical order.$3
`
repo/
βββ fishook.json # team-wide rules (tracked)
βββ .fishook.local.json # personal rules (gitignored)
βββ frontend/
βββ fishook.json # only applies to frontend/
`.gitignore`
.fishook.local.json
`This enables:
* Shared enforcement for teams
* Personal hooks without forking config
* Directory-scoped rules for monorepos
---
From simple to powerful
$3
`json
{
"pre-commit": ["npm test", "npm run lint"],
"commit-msg": "./validate_commit.sh"
}
`$3
`json
{
"pre-commit": [
{
"applyTo": [".js", ".ts"],
"onChange": [
"$FISHOOK_COMMON/forbid-pattern.sh 'console\\.log' 'Remove console.log'"
]
}
]
}
`Fishook lets you react to:
* file adds / changes / deletes
* ref updates
* branch creation
* commit message edits
All without leaving JSON + shell.
---
Full Git hook coverage
Fishook supports every Git hook, including:
* Commit workflow:
pre-commit, commit-msg, prepare-commit-msg, β¦
* Branch & history: pre-rebase, post-checkout, post-merge, β¦
* Push & refs: pre-push, update, post-receive, β¦
* Server-side hooks (self-hosted repos)Most tools focus on pre-commit.
Fishook exposes everything Git exposes.
---
CLI (optional)
`bash
fishook # help
fishook install # install hooks
fishook list # list supported hooks
fishook explain pre-commit
fishook pre-commit # run hook manually
fishook uninstall
`You rarely need the CLI after install.
---
Built-in utilities
Fishook ships with reusable shell helpers in
$FISHOOK_COMMON/:*
forbid-pattern β block secrets or forbidden strings
* forbid-file-pattern β block filenames (e.g. .env)
* ensure-executable β autoβchmod scripts
* modify_commit_message
* pcsed β safely edit staged vs working tree filesThese are optional β you can always write your own shell.
---
Philosophy
Fishook is deliberately:
* Minimal β one file, no plugins
* Explicit β no hidden behavior
* Hackable β shell in, shell out
* Gitβnative β hooks behave exactly as Git defines them
If youβve ever thought βwhy is this so complicated?β when configuring Git hooks β fishook is for you.
---
Reference (configuration + runtime)
This section is the complete reference for:
* environment variables available to hook commands
* the supported JSON shapes
* config discovery & precedence
* install options
---
$3
Fishook loads all files matching
fishook.json found up to 4 directory levels deep.* Files are processed in alphabetical order.
* A config file located in a subdirectory is directory-scoped: file events only apply to files at that directory level or below.
* Top-level keys like
setup and source run as normal regardless of scope.---
$3
At the top level,
fishook.json is a mapping from hook name β action(s), plus optional shared setup keys.#### Minimal
`json
{
"pre-commit": "npm test"
}
`#### Multiple commands
`json
{
"pre-commit": ["npm test", "npm run lint"]
}
`#### Action object
`json
{
"pre-commit": {
"run": "npm test"
}
}
`#### Multiple actions per hook
`json
{
"pre-commit": [
"npm test",
{ "applyTo": ["*.js"], "onChange": ["npm run lint"] }
]
}
`---
$3
onFileEvent is fishookβs most powerful feature.It runs once per file event (add/change/delete/move/copy) and gives you a consistent per-file context via env vars like:
*
FISHOOK_EVENT
* FISHOOK_PATH
* FISHOOK_SRC / FISHOOK_DSTThis lets you write policy checks and auto-fixes that operate on the exact files involved in a commit, push, merge, checkout, etc.
#### When to use
onFileEvent* block secrets or forbidden patterns in changed files
enforce naming rules (no
.env, no .pem, etc.)
* enforce size limits for newly added files
* enforce executable bits on scripts
run targeted formatters only on changed files*#### Minimal example
`json
{
"pre-commit": {
"onFileEvent": [
"$FISHOOK_COMMON/forbid-file-pattern.sh '\.env$' 'Do not commit .env files'",
"$FISHOOK_COMMON/forbid-pattern.sh '(password|secret|api[_-]?key)\s*=' 'Potential secret detected' || true"
]
}
}
`#### Example: enforce script executability
`json
{
"pre-commit": [
{
"applyTo": [".sh", "scripts/*"],
"onFileEvent": ["$FISHOOK_COMMON/ensure-executable.sh"]
}
]
}
`####
applyTo / skipList with onFileEventapplyTo and skipList filter file-event commands (onAdd, onChange, onDelete, onMove, onCopy, onFileEvent).* If
applyTo is omitted, it matches all paths.
* If skipList matches, the file event is ignored.`json
{
"pre-commit": {
"applyTo": ["src/*/.{js,ts,jsx,tsx}"],
"skipList": ["dist/", "vendor/"],
"onFileEvent": ["npm run lint -- $FISHOOK_PATH"]
}
}
`#### Notes
onAdd / onChange / etc. are event-specific* convenience forms.
* onFileEvent is the generic catch-all when you want one handler for all file event types.
* For move/copy events, use FISHOOK_SRC and FISHOOK_DST.---
$3
This mirrors the full supported schema.
`ts
// Basic command forms
type SingleRunCmd = string; // e.g. "npm test"
type RunCmdList = string[]; // e.g. ["npm test", "npm run lint"]
type RunCmd = SingleRunCmd | RunCmdList;// Shared prelude commands
type Setup = RunCmd; // runs BEFORE every command (as-is)
type Source = RunCmd; // runs BEFORE every command (auto-prepends "source")
// Filters (glob patterns)
type FileGlobFilter = string | string[];
// Action specification
type SingleActionSpec = {
run?: RunCmd; // run once per hook
// File events (run per-file)
onAdd?: RunCmdList;
onChange?: RunCmdList;
onDelete?: RunCmdList;
onMove?: RunCmdList;
onCopy?: RunCmdList;
onFileEvent?: RunCmdList; // generic per-file event
// Ref events (run per-ref)
onRefEvent?: RunCmdList;
onRefCreate?: RunCmdList;
onRefUpdate?: RunCmdList;
onRefDelete?: RunCmdList;
// Generic per-event hook entry
onEvent?: RunCmdList;
// File filters (apply to file-event commands)
applyTo?: FileGlobFilter; // defaults to all
skipList?: FileGlobFilter; // defaults to none
};
type SingleAction = RunCmd | SingleActionSpec;
type Action = SingleAction | SingleAction[];
// Hook key names (Git hook names)
type Key =
| 'applypatch-msg'
| 'pre-applypatch'
| 'post-applypatch'
| 'sendemail-validate'
| 'pre-commit'
| 'prepare-commit-msg'
| 'commit-msg'
| 'post-commit'
| 'pre-rebase'
| 'post-checkout'
| 'post-merge'
| 'post-rewrite'
| 'pre-push'
| 'pre-auto-gc'
| 'pre-receive'
| 'update'
| 'post-receive'
| 'post-update'
| 'push-to-checkout'
| 'proc-receive'
| 'fsmonitor-watchman';
type Spec = {
setup?: Setup;
source?: Source;
[k in Key]?: Action;
};
`---
$3
####
setupRuns before every command exactly as written. Useful for PATH fixes, exports, etc.
`json
{ "setup": "export PATH=$HOME/.local/bin:$PATH" }
`####
sourceRuns before every command, but fishook will automatically prepend
source.`json
{ "source": "$FISHOOK_REPO_ROOT/.venv/bin/activate" }
`---
$3
These are available in the shell context where fishook runs commands:
*
old() β print old file content
* new() β print new file content
* diff() β print diff for the current file
* raise "message" β fail the hook with a messageExample:
`json
{
"pre-commit": [
{
"applyTo": ["*.js"],
"onChange": [
"diff | grep -q '+.*TODO' && echo 'Warning: new TODO in $FISHOOK_PATH'"
]
}
]
}
`---
$3
These are available to all commands, including
setup and source.#### Paths & identity
*
FISHOOK_COMMON β directory containing fishookβs bundled helper scripts
* FISHOOK_CONFIG_DIR β directory containing the current config file
* FISHOOK_REPO_ROOT β absolute path to repo root
* FISHOOK_REPO_NAME β repo directory name#### Current hook / event context
*
FISHOOK_HOOK β current hook name
* FISHOOK_EVENT β event type (add, change, delete, move, copy)#### Current file context (file events)
*
FISHOOK_PATH β file path for add/change/delete
* FISHOOK_SRC β source path (move/copy)
* FISHOOK_DST β destination path (move/copy)#### Current ref context (ref events)
*
FISHOOK_REF β ref name
* FISHOOK_OLD_OID β old commit oid
* FISHOOK_NEW_OID β new commit oid#### Remote context (pre-push)
*
FISHOOK_REMOTE_NAME β remote name
* FISHOOK_REMOTE_URL β remote URL---
$3
Fishook does not hide Gitβs native hook arguments; they remain available as
$1, $2, ...Common ones:
*
commit-msg: $1 = path to commit message file
* post-checkout: $1 old HEAD, $2 new HEAD, $3 checkout flag
* pre-push: $1 remote name, $2 remote URL (ref updates are on stdin)---
$3
Fishook installs hook shims into
.git/hooks/.If hooks already exist, fishook can:
* overwrite
* chain
* backup
To bypass the interactive prompt (useful in CI), set:
*
FISHOOK_INSTALL_CHOICE *
1 = overwrite
* 2 = chain
* 3 = backup---
$3
Helpers in
$FISHOOK_COMMON/ (optional):*
forbid-pattern β fail if a regex matches file content
* forbid-file-pattern β fail if a regex matches file path
* ensure-executable β mark the current file executable
* modify_commit_message
* iter_source β source all bash files in a folder
* pcsed β apply sed replacements safelyExample:
`json
{
"pre-commit": [
{
"applyTo": [".sh", "scripts/"],
"onAdd": ["$FISHOOK_COMMON/ensure-executable.sh"],
"onChange": ["$FISHOOK_COMMON/ensure-executable.sh"]
}
]
}
`---
Requirements
*
git
* bash
* jq`Unlicense