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.
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fishook-1.1.1.tar.gz.
File metadata
- Download URL: fishook-1.1.1.tar.gz
- Upload date:
- Size: 33.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5901f8f37a896af34d000a055143de5b69a46ff93131ad07d3753a88a1462b66
|
|
| MD5 |
89bb93414c4ed7749210d08325243388
|
|
| BLAKE2b-256 |
f58e80c22f1ab6ad4156d3a6b7554482b727d0acf819dd5cfa69cff9679945d3
|
Provenance
The following attestation bundles were made for fishook-1.1.1.tar.gz:
Publisher:
publish-to-pip.yml on modularizer/fishook
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fishook-1.1.1.tar.gz -
Subject digest:
5901f8f37a896af34d000a055143de5b69a46ff93131ad07d3753a88a1462b66 - Sigstore transparency entry: 814991906
- Sigstore integration time:
-
Permalink:
modularizer/fishook@d5099cea1fea841da1bbcbef0a2417b8b382b05c -
Branch / Tag:
refs/tags/v1.1.1 - Owner: https://github.com/modularizer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pip.yml@d5099cea1fea841da1bbcbef0a2417b8b382b05c -
Trigger Event:
release
-
Statement type:
File details
Details for the file fishook-1.1.1-py3-none-any.whl.
File metadata
- Download URL: fishook-1.1.1-py3-none-any.whl
- Upload date:
- Size: 34.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4af21b9e1e3e96c44255cd6eca9142825617e4a222d2211fffc477dc25982209
|
|
| MD5 |
a3c4a09a30579802e9f283ef8e63c050
|
|
| BLAKE2b-256 |
a0825d317ee1f2586c4650ce869acb888a57cb7cae65db09cb04aec9e4d2a870
|
Provenance
The following attestation bundles were made for fishook-1.1.1-py3-none-any.whl:
Publisher:
publish-to-pip.yml on modularizer/fishook
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fishook-1.1.1-py3-none-any.whl -
Subject digest:
4af21b9e1e3e96c44255cd6eca9142825617e4a222d2211fffc477dc25982209 - Sigstore transparency entry: 814991909
- Sigstore integration time:
-
Permalink:
modularizer/fishook@d5099cea1fea841da1bbcbef0a2417b8b382b05c -
Branch / Tag:
refs/tags/v1.1.1 - Owner: https://github.com/modularizer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pip.yml@d5099cea1fea841da1bbcbef0a2417b8b382b05c -
Trigger Event:
release
-
Statement type: