amont
Catch the bad commit before it exists — and take the whole thing back out in one command.
A single Rust binary that checks git commit and git push — no YAML to
write, no runtime to install, nothing to configure before it is useful.
- Useful in the first minute. Twenty-one built-in checks — commit-message conventions, merge-conflict markers, the linters and formatters for the languages your repository actually uses, branch rules, your test suite — and each one fires only where the repository has opted into its tool.
- A cloned repository cannot run code on your machine. Checks a repository
declares for itself are inert until you review them and say
amont trust— a gate pre-commit, lefthook and husky do not have. - Nothing on the commit path but
std. The hook binary links no external crates, and CI fails any build that changes that. - Leaving is one command.
amont uninstallremoves exactly the four shims install wrote; a hook you or another tool put there is named and left alone.

How that stacks up against pre-commit, lefthook and husky, feature by feature: the full comparison.
Install
Linux and macOS
|
Windows (PowerShell — the line above is POSIX sh and only reaches
Windows through Git Bash)
irm https://raw.githubusercontent.com/fredericrous/amont/main/install/install.ps1 | iex
Either one downloads a release binary, verifies it against the published
SHA256SUMS, and puts it where the hooks already look — ~/.local/bin, or
%USERPROFILE%\.local\bin. Both enable nothing: hooks are turned on per
repository, by you, afterwards.
&&
Prefer not to pipe a script into a shell? Download a binary and its checksum
from Releases —
prebuilt for Linux (gnu/musl, x86_64 and aarch64), macOS (Intel and Apple
silicon) and Windows. Or build from source: cargo build --release.
From crates.io:
Or with Homebrew:
Uninstall
Uninstall is listed second on purpose. These hooks can block a commit, so the
honest question to answer first is how you get out — and the answer is that
uninstall removes our four shims and nothing else. A hook you wrote
yourself is left where it is and named in the output, whatever it is; a hook it
cannot even read is named too rather than passed over in silence. Your
hook.skip and amont.severity settings are never touched, because those
are your statements about your repository.
This is also why the README does not tell you to run
rm $(git rev-parse --git-dir)/hooks/*. That glob deletes every hook in the
directory — including ones other tools installed and ones you wrote — to remove
four files that belong to us.
To bypass a single run rather than uninstall: git commit --no-verify.
To turn off one check permanently, see Turning a check off, or down.
What actually runs
amont list answers that for the repository you are standing in, and it is
the honest answer rather than the catalogue: most checks are inert in most
repositories, because a repo with no ruff.toml never needs ruff.
pre-commit
● ban-terms
○ cargo-fmt inert here — needs .rs + Cargo.toml
● lint-json-yaml
● merge-conflict
○ prettier inert here — needs .prettierrc | .prettierrc.json | …
● usual-name
pre-push
● branch-protect
● branch-pattern
● pull-rebase
○ cargo-test inert here — needs .rs + Cargo.toml
● runs here ○ inert ⊘ skipped via hook.skip ✗ declaration unusable
Twenty-one built-in checks across four git hooks, plus any your repository declares itself. The full list, with what each one needs before it fires, is in the checks reference.
Two ways to turn hooks on
Per repository — the default, and nothing runs anywhere you did not ask:
&&
amont install --force replaces a hook the installer would otherwise
refuse — one carrying no marker of ours, or a symlink. Even --force never
writes a tracked file or a directory. The full semantics:
what --force will and will not do.
Everywhere, forever — an opt-in, and a real one:
Git copies that directory into .git/hooks on every init and every
clone, so from then on every repository you clone runs these hooks without
being asked again. That is the convenience, and it is worth having.
It is also a standing grant, so it is worth stating what you granted. A cloned
repository can declare its own checks in amont.conf, and with this key
set those are one amont trust away from running on your first commit in a
repository you may have cloned only to read. If you set this, trust
deliberately rather than letting installation be the moment you decided.
One view across every repo
amont-fleet — installed separately, on purpose — answers the questions a
directory full of repositories accumulates: which repos are covered, which
shims went stale after an upgrade, what every check is doing where, and which
repository is quietly carrying a hook.skip somebody forgot.

Design record: the fleet dashboard.
Trust: a repository you clone cannot run its own checks
amont.conf is committed — that is the point, a team shares a check by
committing it. The consequence is that cloning a repository and committing to
it would otherwise run commands that repository chose, and neither of those
acts is one anybody performs as a decision about trust.
So a cloned repository's declared checks are inert until you say otherwise:
The record is keyed on the file's content, not its path, so a git pull
that adds a command does not inherit the consent given to the file before it.
The fingerprint is git hash-object --no-filters, and the --no-filters is
not decoration: plain hash-object applies the clean filter and eol conversion
that the repository's own committed .gitattributes asks for, which would let
a repository choose the transform its own consent is taken through.
Full reasoning: the trust model.
Turning a check off, or down
Every check has an id, <trigger>-<name> — pre-commit-clippy. Three things
name it, and both config surfaces read all three the same way:
amont.severity.<key> takes the same three, and keeps the signal — the
check still runs and still reports, it just stops failing the commit:
Where several keys reach one check the most specific wins: full id, then short
name, then trigger. Nothing matches by substring — hook.skip e reaches
nothing at all, and skipping lint-js leaves lint-json-yaml alone.
A skipped check is announced on every commit, so a config line nobody remembers writing cannot go on silently disabling things. More in opting out and configuration.
Making the commit convention yours
commit-msg is the one hook hook.skip and --no-verify cannot reach, so its
opinions are adjustable in themselves:
It asks where the type's gitmoji goes — none (the default: your subject,
untouched), prefix, suffix or replace — and for the subject limit, the
description budget and the body wrap column. Then it prints the exact
git config lines it wrote, so you can paste them into your dotfiles or hand
them to a teammate.
The limits measure what you wrote, so a gitmoji never eats your budget, and re-running over an amended message changes nothing. Full reference: commit conventions.
Custom checks
A repository can declare checks of its own in a committed amont.conf:
# stage name scope severity command
pre-commit shellcheck *.sh block scripts/lint-shell.sh
pre-push smoke * warn make smoke
They run alongside the built-ins, obey the same hook.skip and
amont.severity controls, and are inert until trusted. Full reference:
custom checks.
Running the checks yourself
The two questions are different on purpose. --all-files on a dirty tree
reports on content that is not committed and may never be — which is what you
want when adopting a check into an existing repository, where git add . is
not an acceptable way to measure the mess.
What a push actually tests
By default pre-push runs your suite against the working tree, and says
so. That is fast and usually what you want, but it is not what you are pushing:
an uncommitted fix makes a broken commit look green.
turns on the accurate answer — the suite runs in a throwaway checkout of the
commits being pushed, and your tree is not touched. It costs a second checkout
and a build that cannot reuse your target/ cache, which is why it is opt-in.
For coding agents
amont list --json is the same answer as amont list, machine-readable:
every check's declared and effective severity, whether it fires here and why
not, and its command if it is a declared external. --stage filters to one
trigger; --pushed scopes to what your next push would carry (@{u}..HEAD)
rather than the whole tracked tree.
agents-md only ever touches a <!-- amont:start --> / <!-- amont:end -->
span, so the rest of the file stays yours.
The block it writes also warns the agent that git commit and git push
run their checks first — pre-commit can mean clippy building a workspace,
pre-push a whole test suite — and an agent whose shell tool defaults to a
two-minute timeout will kill the command mid-check and read its own
impatience as a failure. Ten minutes is the safe floor, for both.
Why you can let this near your commits
A prompt theme is cosmetic. This blocks commits and pushes, reads every staged file, and runs with your credentials while nobody is watching — so the claim it has to earn is not "delightful", it is "harmless".
- The commit path links no external crates.
amontandamont-runtimeare std-only, andscripts/check-no-deps.shfails a build that changes that — fails closed, so a cargo error or an unreachable registry is a failure rather than a reassuring green tick.amont-fleettakes dependencies quite happily; it is installed separately and runs when asked. - No network, ever. The binary phones nothing home — no telemetry, no update checks, no fetches. With the commit path std-only, there is not even an HTTP client linked to do it with.
- Over six hundred tests, run on Linux, macOS and Windows, alongside
cargo fmt --check,clippy -D warnings, an MSRV floor of 1.74 compiled for the commit path, andcargo-audit. - v1.0.0 followed a full security review, and each finding landed with a
committed reproduction: a drive-by RCE via a relative path in the shim; a
held-store format that let a repository delete a tracked file and plant a
symlink outside the worktree; a trust prompt a repository could conceal
declarations from; guards that wrote through symlinks onto tracked source; a
commit subject shaped like a trailer destroying commit trailers;
pre-commit-pyrightblocking every commit it ran on. - Your uncommitted work is the thing that must never be lost. The release
profile deliberately omits
panic = "abort"so that theDropthat restores unstaged work still runs when a check panics — with a test asserting on the manifest, because cargo ignores that setting for test targets and no behavioural test could catch the regression.
Threat model and private reporting: SECURITY.md.
Windows
Everything works, and the PowerShell one-liner in Install is all
the setup most machines need. One difference: there is no symlink. To build
from source instead — Git for Windows ships bash and coreutils but not
make:
On macOS/Linux ~/.config/git/git-templates is usually a symlink to the
checkout, so init.templateDir can point at a stable XDG path. Windows does
not create symlinks without Developer Mode or elevation, so point git straight
at the checkout instead:
Nothing else changes. The shims never need the symlink: they resolve the binary
at runtime, trying $GIT_HOOKS_BIN, the baked path, ~/.local/bin/amont
and ~/.local/bin/amont.exe, then PATH.
Requirements
- Git 2.31+ —
git rev-parse --path-format=absolutelanded there, and three places depend on it. On an older git those return nothing rather than failing loudly, which is the worst shape for a version floor: the tool appears to work and quietly resolves the wrong paths.
The hooks are a single binary with no runtime dependencies. Each check brings its own tool requirement only where you have opted into that check.
Documentation
The full documentation is in docs/, versioned with the code and
published as a book:
- Installing and activating
- The checks · Configuration · Opting out
- The trust model · Custom checks
- Where the hooks fit in your flow · Commit conventions · How it compares · Ideas, not a roadmap
- Decision records for maintainers: hook architecture, index fidelity and run modes, skip management, the fleet dashboard, the Rust migration
Contributing
Everything is Rust, in crates/:
crates/amont-runtime/ the checks, registry and dispatchers. std only.
crates/amont/ the hook binary. Runs on every commit. std only.
crates/amont-fleet/ the dashboard and the fleet fixer. Opt-in.
make check is the CI-parity target — run it before you push. Setup, the
zero-dependency rule and when reopening it is legitimate, the house test style
and the commit convention are all in CONTRIBUTING.md.
Questions, "does this work with X", ideas for checks: Discussions. Bugs: issues.
By participating you agree to the Code of Conduct.
License
MIT.