Smee - the right hand of (Git) hooks
git-smee is a lightweight Rust CLI that manages Git hooks from a version-controlled configuration file, .git-smee.toml, in your repository.
Instead of copying hook scripts around or relying on heavy external tooling, git-smee installs small, idempotent hook wrappers that delegate to commands defined in your config—making hook behavior consistent across all contributors.
- Version-controlled: Define hooks once in
.git-smee.toml, commit it, share it. - Cross-platform minded: Designed to work on Unix and Windows (Git Bash) via a clear platform abstraction.
- Portable Unix execution: Hook commands run through POSIX
sh -con Unix-like systems (not Bash-specific). - Small and focused: Single-purpose binary, no plugin ecosystem or extra runtime.
brew tap errfld/git-smee
brew install git-smeeThe Homebrew tap installs prebuilt release artifacts for Apple Silicon macOS (aarch64-apple-darwin), Intel macOS (x86_64-apple-darwin), and Intel Linuxbrew (x86_64-unknown-linux-gnu).
cargo install git-smee-cliThe minimum supported Rust version is 1.88.0.
git clone https://github.com/errfld/git-smee.git
cd git-smee
cargo install --path crates/git-smee-cli-
Initialize a configuration file in your repository:
git smee init
This creates a
.git-smee.tomlfile with a default pre-commit hook. If the file already exists,initrefuses to overwrite it unless you pass--force.You can start from an explicit built-in template and then edit the generated TOML:
git smee init --template rust git smee init --template node-pnpm git smee init --template generic
Available templates are:
minimal(default): a single starter pre-commit command.rust:cargo fmt --all -- --check,cargo clippy --workspace --all-targets --all-features -- -D warnings, andcargo test --workspace --all-targets --all-features.node-pnpm:pnpm lintandpnpm testhooks.generic: an editable shell-command starter with commented examples.
Unknown template names are rejected with an error that lists valid template names.
-
Edit
.git-smee.tomlto define your hooks:[[pre-commit]] command = "cargo fmt --check" [[pre-commit]] command = "cargo test" [[pre-push]] command = "cargo test --all-targets"
-
Install the hooks into
.git/hooks:git smee install
By default,
installonly overwrites hook files previously managed by git-smee. Existing unmanaged hook files are preserved unless you pass--force. When a hook phase is removed from.git-smee.toml,installalso prunes the now-obsolete managed wrapper for that phase so stale hooks do not keep running; unmanaged files for removed phases are left untouched. If the repository usescore.hooksPathand that directory does not exist yet,installcreates the effective hooks directory before writing managed wrappers.
That's it! Your hooks are now active. When Git triggers a hook, the installed wrapper runs the git-smee executable directly and executes the configured commands in order.
By default, git-smee reads .git-smee.toml from the repository root.
You can override the config path in two ways:
- CLI flag:
--config <path> - Environment variable:
GIT_SMEE_CONFIG=<path>
Precedence is explicit: --config > GIT_SMEE_CONFIG > .git-smee.toml.
Examples:
git smee install --config .config/git-smee.toml
GIT_SMEE_CONFIG=.config/git-smee.toml git smee run pre-commitThe .git-smee.toml file uses TOML format. Each hook is defined as an array of tables:
[[hook-name]]
command = "your command here"| Field | Type | Required | Description |
|---|---|---|---|
command |
string | yes | The command to execute |
parallel_execution_allowed |
bool | no | Allow parallel execution with other parallel-enabled commands (default: false) |
When running hooks, git-smee executes commands in two phases:
- Sequential phase: All commands with
parallel_execution_allowed = false(or omitted) run one at a time, in the order they appear in the config. - Parallel phase: All commands with
parallel_execution_allowed = truerun concurrently using a thread pool.
Sequential commands always complete before parallel commands begin. If any sequential command fails, execution stops immediately. For parallel commands, the first failing command causes the overall run to fail, but commands that are already in flight may still finish; git-smee reports failures deterministically by configured parallel command order for the commands that were attempted.
After each git smee run <hook>, git-smee prints a compact summary after the hook command output:
Hook summary: pre-commit
total: 2 attempted, 0 skipped, 0 failed in 128ms
sequential: 1 attempted, 0 failed in 48ms
parallel: 1 attempted, 0 failed in 80ms
- sequential command #1: ok in 48ms
- parallel command #1: ok in 80ms
Failed runs include the first failing command and exit code while preserving the command's own output:
Hook summary: pre-commit
total: 1 attempted, 1 skipped, 1 failed in 12ms
sequential: 1 attempted, 1 failed in 12ms
parallel: 0 attempted, 0 failed in 0ms
- sequential command #1: failed with code 1 in 12ms
first failure: sequential command #1 exited with code 1
Example:
[[pre-commit]]
command = "cargo fmt --check"
# Sequential (default) - runs first
[[pre-commit]]
command = "cargo clippy"
parallel_execution_allowed = true
# Parallel - runs concurrently with other parallel commands
[[pre-commit]]
command = "cargo test --lib"
parallel_execution_allowed = true
# Parallel - runs concurrently with clippy
[[pre-commit]]
command = "echo 'Setup complete'"
# Sequential - runs before parallel commandsIn this example, the two sequential commands (cargo fmt --check and echo 'Setup complete') run first in order, then cargo clippy and cargo test --lib run in parallel.
git-smee supports all standard Git lifecycle hooks:
| Hook | Description |
|---|---|
applypatch-msg |
Edit the commit message of a patch |
pre-applypatch |
Run before a patch is applied |
post-applypatch |
Run after a patch is applied |
pre-commit |
Run before a commit is created |
prepare-commit-msg |
Prepare the default commit message |
commit-msg |
Validate or modify the commit message |
post-commit |
Run after a commit is created |
pre-merge-commit |
Run before a merge commit is created |
pre-rebase |
Run before a rebase starts |
post-checkout |
Run after a checkout |
post-merge |
Run after a merge |
post-rewrite |
Run after commands that rewrite commits |
pre-push |
Run before a push |
pre-receive |
Run before refs are updated (server-side) |
update |
Run once per ref update (server-side) |
proc-receive |
Handle receive-pack commands (server-side) |
post-receive |
Run after refs are updated (server-side) |
reference-transaction |
Run when reference transaction state changes |
push-to-checkout |
Run when a push tries to update the checked-out branch |
pre-auto-gc |
Run before automatic garbage collection |
post-update |
Run after refs are updated (server-side) |
fsmonitor-watchman |
Integration with watchman file monitor |
post-index-change |
Run after the index is written |
When Git invokes a hook with positional arguments (for example commit-msg <path> or
post-checkout <old> <new> <flag>), installed git-smee wrappers forward those arguments to
git smee run.
Forwarded hook arguments are available as shell positional parameters inside configured
commands: $1, $2, ... on Unix and %1, %2, ... on Windows. Examples:
[[commit-msg]]
command = "test -n \"$1\""[[commit-msg]]
command = "if \"%1\"==\"COMMIT_EDITMSG\" exit /b 0"On all platforms, git-smee also exports forwarded hook args as environment variables:
GIT_SMEE_HOOK_ARGC: number of forwarded argsGIT_SMEE_HOOK_ARG_1...GIT_SMEE_HOOK_ARG_N: individual argument values
When git smee run receives stdin, it buffers that payload once and replays identical bytes to
each configured command for the hook. This keeps stdin-driven hooks such as pre-push,
pre-receive, and post-receive deterministic even when multiple commands are configured for
the same phase. The default buffered-stdin limit is 10 MiB; set
GIT_SMEE_HOOK_STDIN_LIMIT_BYTES=<bytes> to raise or lower it for unusually large hooks.
proc-receive is an interactive pkt-line protocol, so git-smee does not pre-buffer stdin for
that phase. The configured proc-receive command inherits stdin directly to avoid blocking the
protocol handshake before the command starts.
git smee init [--force] [--config <path>] # Initialize a config file
git smee install [--force] [--config <path>] # Install hooks from the selected config
git smee [--config <path>] run <hook> [hook-args...] # Run a specific git hook
git smee [--config <path>] status [--json] # Show hook coverage and drift
git smee [--config <path>] doctor [--json] # Diagnose repository setup and hook drift
git smee migrate-hooks # Suggest config entries for existing hooksRun git smee status as a read-only onboarding or CI smoke check after editing
.git-smee.toml or reinstalling wrappers. It summarizes each configured phase,
command counts, missing/unmanaged/stale wrappers, obsolete managed wrappers, and
next actions. For example:
git-smee status: Drift
configured hooks:
- pre-commit: configured commands=1, installed (.git/hooks/pre-commit)
- pre-push: configured commands=1, missing (.git/hooks/pre-push)
next: run git smee install to create pre-push
obsolete managed hooks:
- commit-msg: obsolete managed wrapper (.git/hooks/commit-msg)
next: remove obsolete managed hook .git/hooks/commit-msg
next actions:
- remove obsolete managed hook .git/hooks/commit-msg
- run git smee install to create pre-push
Use git smee status --json when tooling needs stable fields such as status,
hooks[].configured_command_count, hooks[].state, obsolete_managed_hooks, and
next_actions.
Run git smee migrate-hooks before replacing an existing .git/hooks/* setup.
It is read-only: unmanaged Git hook files are reported as parseable TOML snippets
that call a preserved legacy copy outside the managed hooks directory (for
example .git-smee/legacy/pre-commit), while git-smee-managed wrappers are
ignored and listed in a comment. Move each legacy script as instructed before
copying the suggestions into .git-smee.toml and running git smee install.
Run git smee doctor when onboarding a repository, after changing core.hooksPath, or when a
hook does not fire as expected. The human-readable report groups ok, warnings, and errors
with remediation commands; --json emits the same stable fields for automation. Doctor exits
successfully when no errors are present and exits non-zero when setup errors need action.
-
You declare hooks in
.git-smee.toml:[[pre-commit]] command = "cargo fmt --check" [[pre-commit]] command = "cargo test" [[pre-push]] command = "cargo test --all-targets"
-
git-smee reads this config and knows:
- which Git hook name (e.g.
pre-commit) maps to which commands, - that each
[[hook-name]]entry is an orderedHookDefinition.
- which Git hook name (e.g.
-
The installer will write idempotent scripts into Git's effective hooks directory:
- Each script runs the installed
git-smeeexecutable directly with--config <resolved path> run <hook>, forwarding original Git hook positional arguments.
- Each script runs the installed
-
The executor will run the configured commands for that hook and propagate exit codes back to Git.
This repo is a Cargo workspace with two crates:
crates/git-smee-core- Library crate with all domain logic:
.git-smee.tomlparsing (SmeeConfig,HookDefinition)- Error types using
thiserror - Installer, executor, platform abstraction
- Library crate with all domain logic:
crates/git-smee-cli- Binary crate providing the
git smeeCLI:- Uses
clapfor argument parsing - Thin wrapper that delegates to
git-smee-core
- Uses
- Binary crate providing the
Early-stage / experimental. Expect breaking changes while the design settles.
From the repo root:
cargo build
cargo test
cargo run -p git-smee-cli -- --helpFor local CI parity checks:
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
rustup run stable cargo llvm-cov --workspace --all-features --locked --summary-only -- --skip repository::tests::
cargo auditGitHub Actions CI validates Linux (stable/beta/nightly), macOS (stable), Windows (stable), and MSRV (1.88.0) build/test compatibility.
Tests that mutate process-global state (environment variables or the current working directory) must hold the shared process_state_lock test helper for the entire setup/exercise/restore window. Do not add module-local env/cwd mutexes: Rust 2024 treats environment mutation as unsafe unless all process-wide access is externally synchronized.
Release archives publish adjacent .sha256 files. Verify downloads with:
shasum -a 256 -c git-smee-<version>-<target>.tar.gz.sha256MIT © 2025 Eran Riesenfeld