fishook 🐟🪝
Git 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.
Why fishook?
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,gitandsed.
Common use cases
- 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
Quickstart
npm install --save-dev fishook # runs fishook install via postinstall
# or: pipx install fishook
# or: pip install fishook
fishook install # install all hooks
# fishook install pre-commit commit-msg
Minimal fishook.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*.jsonfiles - Commands are executed as-written using
bash - No background daemons, caches, or magic state
If you can write a shell command, you can write a fishook rule.
Feature overview
- connect to ANY hook
gitexposes - use
onFileEventto run a command per file changed - use
applyToandskipListto apply your command to only specific files - use
sourceto setup your environment before running commands - use helpers whcih are already in the shell scope, like
new,old,diff,modifyto see file changes and update files in the staging area - use
raiseto fail the hook with a message - use multiple
fishook.jsonfiles 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.
Common pattern
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
Simple
{
"pre-commit": ["npm test", "npm run lint"],
"commit-msg": "./validate_commit.sh"
}
File-aware and event-driven
{
"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)
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 stringsforbid-file-pattern– block filenames (e.g..env)ensure-executable– auto‑chmod scriptsmodify_commit_messagepcsed– safely edit staged vs working tree files
These 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
Config discovery & order
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
setupandsourcerun as normal regardless of scope.
Supported JSON shapes
At the top level, fishook.json is a mapping from hook name → action(s), plus optional shared setup keys.
Minimal
{
"pre-commit": "npm test"
}
Multiple commands
{
"pre-commit": ["npm test", "npm run lint"]
}
Action object
{
"pre-commit": {
"run": "npm test"
}
}
Multiple actions per hook
{
"pre-commit": [
"npm test",
{ "applyTo": ["*.js"], "onChange": ["npm run lint"] }
]
}
The big one: onFileEvent
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_EVENTFISHOOK_PATHFISHOOK_SRC/FISHOOK_DST
This 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
{
"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
{
"pre-commit": [
{
"applyTo": ["*.sh", "scripts/**"],
"onFileEvent": ["$FISHOOK_COMMON/ensure-executable.sh"]
}
]
}
applyTo / skipList with onFileEvent
applyTo and skipList filter file-event commands (onAdd, onChange, onDelete, onMove, onCopy, onFileEvent).
- If
applyTois omitted, it matches all paths. - If
skipListmatches, the file event is ignored.
{
"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.onFileEventis the generic catch-all when you want one handler for all file event types.- For move/copy events, use
FISHOOK_SRCandFISHOOK_DST.
Reference type model
This mirrors the full supported schema.
// 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;
};
Top-level keys
setup
Runs before every command exactly as written. Useful for PATH fixes, exports, etc.
{ "setup": "export PATH=$HOME/.local/bin:$PATH" }
source
Runs before every command, but fishook will automatically prepend source.
{ "source": "$FISHOOK_REPO_ROOT/.venv/bin/activate" }
Built-in functions available in hook commands
These are available in the shell context where fishook runs commands:
old()– print old file contentnew()– print new file contentdiff()– print diff for the current fileraise "message"– fail the hook with a message
Example:
{
"pre-commit": [
{
"applyTo": ["*.js"],
"onChange": [
"diff | grep -q '+.*TODO' && echo 'Warning: new TODO in $FISHOOK_PATH'"
]
}
]
}
Environment variables
These are available to all commands, including setup and source.
Paths & identity
FISHOOK_COMMON– directory containing fishook’s bundled helper scriptsFISHOOK_CONFIG_DIR– directory containing the current config fileFISHOOK_REPO_ROOT– absolute path to repo rootFISHOOK_REPO_NAME– repo directory name
Current hook / event context
FISHOOK_HOOK– current hook nameFISHOOK_EVENT– event type (add, change, delete, move, copy)
Current file context (file events)
FISHOOK_PATH– file path for add/change/deleteFISHOOK_SRC– source path (move/copy)FISHOOK_DST– destination path (move/copy)
Current ref context (ref events)
FISHOOK_REF– ref nameFISHOOK_OLD_OID– old commit oidFISHOOK_NEW_OID– new commit oid
Remote context (pre-push)
FISHOOK_REMOTE_NAME– remote nameFISHOOK_REMOTE_URL– remote URL
Git hook positional arguments
Fishook does not hide Git’s native hook arguments; they remain available as $1, $2, ...
Common ones:
commit-msg:$1= path to commit message filepost-checkout:$1old HEAD,$2new HEAD,$3checkout flagpre-push:$1remote name,$2remote URL (ref updates are on stdin)
Install behavior & options
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_CHOICE1= overwrite2= chain3= backup
Built-in common utilities
Helpers in $FISHOOK_COMMON/ (optional):
forbid-pattern <pattern> <message>– fail if a regex matches file contentforbid-file-pattern <pattern> <message>– fail if a regex matches file pathensure-executable– mark the current file executablemodify_commit_messageiter_source <folder>– source all bash files in a folderpcsed <pattern> <replacement> [--index-only] [--local-only]– apply sed replacements safely
Example:
{
"pre-commit": [
{
"applyTo": ["*.sh", "scripts/*"],
"onAdd": ["$FISHOOK_COMMON/ensure-executable.sh"],
"onChange": ["$FISHOOK_COMMON/ensure-executable.sh"]
}
]
}
Requirements
gitbashjq
License
Unlicense
Metadata
Release files for fishook 1.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fishook-1.1.1.tar.gz | 33.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fishook-1.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 67.6 kB
Release files / fishook-1.1.1.tar.gz
| Download URL | fishook-1.1.1.tar.gz |
|---|---|
| Size | 33.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5901f8f37a896af34d000a055143de5b69a46ff93131ad07d3753a88a1462b66
|
|
BLAKE2b-256 checksum How to use checksums |
f58e80c22f1ab6ad4156d3a6b7554482b727d0acf819dd5cfa69cff9679945d3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jan 12, 2026.
Transparency logRelease files / fishook-1.1.1-py3-none-any.whl
| Download URL | fishook-1.1.1-py3-none-any.whl |
|---|---|
| Size | 34.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4af21b9e1e3e96c44255cd6eca9142825617e4a222d2211fffc477dc25982209
|
|
BLAKE2b-256 checksum How to use checksums |
a0825d317ee1f2586c4650ce869acb888a57cb7cae65db09cb04aec9e4d2a870
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jan 12, 2026.
Transparency log