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. Thirty-seven 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 five 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 (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. Both enable
nothing — hooks are turned on per repository, by you, afterwards:
&&
Homebrew, crates.io, npm, prebuilt binaries for six targets, building from
source, and what --force will and will not do:
installing and activating.
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
Thirty-seven built-in checks across five git hooks, plus any your repository declares itself. What each one needs before it fires: the checks reference.
Turning hooks on
Per repository is the default, and nothing runs anywhere you did not ask:
&&
Everywhere, forever is an opt-in, and a real one:
One command per machine: every future git clone and git init arrives with
the hooks. --conventions declared keeps the house rules — commit shapes,
branch names, gates — scoped to repositories that commit an amont.conf,
while the safety net of conflict markers, secrets, oversized files and debug
leftovers runs everywhere.
That split is what makes the grant safe on a machine that also clones other
people's projects. It is still a standing grant, so it is worth stating what
you granted: a cloned repository's own checks are then one amont trust away
from running on your first commit in a repository you may have cloned only to
read. Trust deliberately rather than letting installation be the moment
you decided. The full reasoning, and rolling this out to a team:
team rollout.
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 a line rather than by everybody installing something. So a
repository you cloned can declare checks, and running them is a decision you
make, not one git clone makes for you.
Until then the declarations are reported, not dropped — you can see there
is a decision waiting. Acceptance is recorded against the file's content, so
a git pull that adds a command does not inherit the trust you gave the file
before it. The trust model.
Custom checks, and packs
A repository declares checks of its own in a committed amont.conf — five
whitespace-separated fields, no shell:
# stage name scope severity command
pre-commit lint-shell *.sh block scripts/lint-shell.sh
pre-commit rubocop *.rb+.rubocop.yml block rubocop
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.
The + in the scope column splits what the change touches from what the
repository carries: *.rb+.rubocop.yml reads "a staged .rb, and this
repository has a .rubocop.yml". That is what lets a check be safe to hand to
somebody else.
Shipping one — packs
A pack is any git repository with an amont.pack at its root, written in
that same syntax. amont add vendors its rows into your amont.conf:
What ships is text, not execution. The rows land between markers with the
pack's commit id beside them, the manifest's fingerprint changes, and every
declared check is inert until you amont trust it. Adding a pack is never the
moment anything becomes runnable.
Want to write one? fredericrous/amont-pack-java
is a complete worked example — two rows, one for Maven and one for Gradle, so
half of it is visibly inert wherever you install it. Its README is the
long-form authoring guide: gating your rows, what a pack may not carry, and
how to publish and tag one.
Full reference for both: custom checks.
Day to day
# staging; `file:line: message` an editor can parse
amont run --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.
Turning one off, or down:
Prefer the downgrade: it keeps the check working and keeps you looking at what it finds. Opting out · configuration · commit conventions.
What a push actually tests. By default pre-push runs your suite against
the working tree, and says so — fast, and usually what you want, but not what
you are pushing. git config amont.testPushedTree true runs it in a throwaway
checkout of the commits being pushed instead.
For coding agents. amont list --json is the same answer as amont list,
machine-readable: declared and effective severity, whether each check fires
here and why not, and the command if it is a declared external. --stage
filters to one trigger, --pushed scopes to what your next push would carry.
amont agents-md writes the guidance block into AGENTS.md.
Where the hooks fit in your flow.
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, and which repository is quietly carrying a
hook.skip somebody forgot.

Design record: the fleet dashboard.
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. - No network, ever. 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 a thousand 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 plant a symlink outside the worktree, a trust prompt a repository could conceal declarations from.
- Your uncommitted work is the thing that must never be lost. The release
profile deliberately omits
panic = "abort"so theDropthat restores unstaged work still runs when a check panics — with a test asserting on the manifest, because no behavioural test could catch that regression.
Threat model and private reporting: SECURITY.md.
Requirements
Git 2.31+ — git rev-parse --path-format=absolute landed 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 hooks are a single binary with no runtime dependencies. Each check brings its own tool requirement only where you have opted into that check.
Everything works on Windows; the one difference is that there is no symlink,
so init.templateDir points straight at the checkout. Details.
The repositories around it
Three companions, each its own repository because it runs somewhere amont deliberately does not:
- amont-agent — a Claude
Code
PreToolUsehook for the mistake no git hook can reach, because it lives in the command string itself:git push … | tail -5reports tail's exit status, so a rejected push reads as success. The guard judges the pipeline before it runs. Independent by design — no shared code, and neither needs the other; they meet in one optional place, where its session notice asksamont agents-md --checkwhether the guidance block an agent is about to believe has gone stale. - attest — the CI half of
amont.attest: when every pre-push block gate passed locally, amont leaves a signed note on the tree it tested, and this single-purpose verifier lets CI skip work provably already done. Fail-open by construction, and separate precisely so that amont itself never runs in CI — the reasoning. - amont-pack-java — the worked example of a pack: how checks amont deliberately does not build in get shipped anyway.
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 and packs
- Where the hooks fit in your flow · Commit conventions · CI, and why amont does not run in it · 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.