Skip to main content

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, git and sed.

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*.json files
  • 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 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.

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 strings
  • forbid-file-pattern – block filenames (e.g. .env)
  • ensure-executable – auto‑chmod scripts
  • modify_commit_message
  • pcsed – 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 setup and source run 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_EVENT
  • FISHOOK_PATH
  • FISHOOK_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 applyTo is omitted, it matches all paths.
  • If skipList matches, 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.
  • 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.

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 content
  • new() – print new file content
  • diff() – print diff for the current file
  • raise "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 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

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 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)

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_CHOICE

    • 1 = overwrite
    • 2 = chain
    • 3 = backup

Built-in common utilities

Helpers in $FISHOOK_COMMON/ (optional):

  • forbid-pattern <pattern> <message> – fail if a regex matches file content
  • forbid-file-pattern <pattern> <message> – fail if a regex matches file path
  • ensure-executable – mark the current file executable
  • modify_commit_message
  • iter_source <folder> – source all bash files in a folder
  • pcsed <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

  • git
  • bash
  • jq

License

Unlicense

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fishook-1.1.1.tar.gz (33.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fishook-1.1.1-py3-none-any.whl (34.6 kB view details)

Uploaded Python 3

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

Hashes for fishook-1.1.1.tar.gz
Algorithm Hash digest
SHA256 5901f8f37a896af34d000a055143de5b69a46ff93131ad07d3753a88a1462b66
MD5 89bb93414c4ed7749210d08325243388
BLAKE2b-256 f58e80c22f1ab6ad4156d3a6b7554482b727d0acf819dd5cfa69cff9679945d3

See more details on using hashes here.

Provenance

The following attestation bundles were made for fishook-1.1.1.tar.gz:

Publisher: publish-to-pip.yml on modularizer/fishook

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for fishook-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4af21b9e1e3e96c44255cd6eca9142825617e4a222d2211fffc477dc25982209
MD5 a3c4a09a30579802e9f283ef8e63c050
BLAKE2b-256 a0825d317ee1f2586c4650ce869acb888a57cb7cae65db09cb04aec9e4d2a870

See more details on using hashes here.

Provenance

The following attestation bundles were made for fishook-1.1.1-py3-none-any.whl:

Publisher: publish-to-pip.yml on modularizer/fishook

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page