# amont-agent
**Your agent just ran `git push … | tail -5`, the push was rejected, and the
command reported success.**
A pipeline's exit status is its **last** command's. `tail` succeeds at tailing
an error message — so a rejected push, a push killed by a timeout, and a push
that never left the machine all look identical, and the trimming throws the
error text away too. The failure is silent in both channels, and the model
reads its own impatience as a green tick.
No git hook can catch that. The mistake is in the command string and never
reaches one.
`amont-agent` is a Claude Code `PreToolUse` hook that reads a shell command
before it runs, and can observe it, advise against it, or refuse it.
[](https://github.com/fredericrous/amont-agent/actions/workflows/ci.yaml)
[](LICENSE)
```console
`git push` pipes into `tail`, so the pipeline reports tail's exit status,
not git push's. A failed, rejected or timed-out run reads as success, and
the trimming discards the error text as well.
→ Run `git push` on its own and read its output afterwards. Then verify the
effect rather than the exit code.
## Install
```sh
Then wire it in — a separate, deliberate step, because a program that can
refuse your agent's commands should not add itself to your settings as a side
effect of you downloading it:
```sh
amont-agent install # prints the settings block, writes nothing
amont-agent install --write # merges it into ~/.claude/settings.json
amont-agent doctor # installed, runnable, and actually firing?
```
Also: `brew install fredericrous/tap/amont-agent` · `cargo install amont-agent`
· [prebuilt binaries](https://github.com/fredericrous/amont-agent/releases/latest)
for Linux (gnu/musl, x86_64 and aarch64), macOS (Intel and Apple silicon) and
Windows.
## It measures before it blocks
Five of the six rules ship as `observe` — they record and say nothing at all.
That is not timidity, it is the method:
| `observe` | records the firing and says nothing |
| `advise` | puts the reason into the model's context; refuses nothing |
| `deny` | refuses the tool call, with the reason and the remedy |
`observe` and `advise` are not two ways of saying "not blocking yet".
`additionalContext` enters the model's context and changes its behaviour,
which contaminates the rate the observation exists to measure. **A rule that
talks is intervening.**
So a rule is promoted from your own transcripts, not from an argument:
```sh
amont-agent backtest --since 2026-07-06 # firings per 1,000 tool calls, weekly
amont-agent explain pipe-to-tail --format cases >> tests/corpus/pipe-to-tail.cases
$EDITOR tests/corpus/pipe-to-tail.cases # each `?` becomes match or nomatch
amont-agent corpus check # and this runs in the test suite
amont-agent graduate pipe-to-tail --to deny
```
`pipe-to-tail` is the only rule that blocks, and it blocks because seven
consecutive weeks of measurement showed no downward trend while every other
habit halved. A habit the model is already correcting does not need a `deny`.
Demotion is not gated at all — `amont-agent demote <rule>`, no questions. A
guard that is hard to back out of is one people uninstall instead of demoting,
and uninstalling takes every rule with it.
[The full method](docs/measuring.md).
## The rules
| `pipe-to-tail` | `deny` | a mutating command whose status is swallowed by a pipe |
| `bare-stash-pop` | `observe` | `git stash pop` with no ref, where `refs/stash` is shared across worktrees |
| `gh-pr-merge-auto` | `observe` | `--auto` on a repo with no required checks, which merges immediately |
| `no-verify` | `observe` | turning the whole commit gate off rather than one check |
| `git-add-broad` | `observe` | staging the tree instead of the change |
| `stale-base` | `advise` | a branch or worktree started from a checkout the remote has moved past |
| `push-preflight` | `advise` | a `git push` whose slow pre-push test gate has not been rehearsed with `amont run pre-push` |
`stale-base` advises from the start because it refuses nothing and names a
failure no correcting loop can see: **nothing fails when you build on stale
code.** The work is correct against the code it can see, and the conflict
arrives later, from somewhere else. So a session opening in a checkout the
remote has moved past is told — after a five-second, one-branch fetch that
never pulls. [Why it never pulls](docs/session-notice.md).
`push-preflight` advises for the same reason. git opens its connection to
the remote *before* it runs `pre-push` and holds it idle for as long as the
test gate takes; a remote that closes idle sessions kills the push after the
gate has already passed, and the model reads "the network" where the cause
was the gate's placement. With amont ≥ 1.27, `amont run pre-push` runs the
same gate with no connection open and stamps the tree, so the push that
follows skips the suite. The rule speaks only when `confirm` finds all three
facts: amont guards this repository's pushes, a test gate would run for this
push, and `HEAD`'s tree carries no stamp yet.
## What it will not do
- **It never emits `allow`.** Approving everything it has no objection to would
switch off the permission system it was installed beside. Silence is how it
says "no objection".
- **Every failure path is silence.** An unreadable payload, an unknown event, a
command it cannot parse, a rule that panics — all exit 0 having written
nothing. A hook that fails toward *refusing* gets deleted from
`settings.json`, which switches off every rule at once; one that fails toward
silence loses a single firing.
- **It does not judge what it cannot read.** Heredocs without terminators,
`eval`, `sh -c`, unbalanced quotes — opaque never fires.
- **It does not phone home.** No telemetry, no update checks. Every firing is
journalled to `~/.claude/amont-agent/journal.log`, redacted, and it only
counts — nothing in it may participate in a decision.
- **A cloned repository cannot change a stance.** Promotion power lives in your
own git config, never in a committed file.
[The reasoning in full](docs/refusals.md).
## Turning it down
```sh
git config --global amont.agent.pipe-to-tail.stance observe # one rule
git config --global amont.agent.stance observe # all of them
AMONT_AGENT_OFF=1 # this shell
amont-agent uninstall --write # remove the entries
```
## Documentation
[Installing](docs/install.md) · [Stances](docs/stances.md) ·
[The rules](docs/rules.md) ·
[Measuring and graduating](docs/measuring.md) ·
[The session notice](docs/session-notice.md) ·
[Configuration](docs/configuration.md) ·
[What it will not do](docs/refusals.md)
## Contributing
```sh
make check # fmt, clippy -D warnings, tests — exactly what CI runs
```
One crate, `serde` and `serde_json` its only dependencies, and both only for
*reading* — Claude Code's payload and your `settings.json`, two shapes defined
by somebody else. What this binary writes is emitted by a hand-rolled escaper,
so the reading and the writing share no representation.
## Related
[amont](https://github.com/fredericrous/amont) — git hooks that catch a bad
commit before it exists, by the same author. Independent of this: no shared
code, and neither needs the other. They meet in one optional place, described
in [the session notice](docs/session-notice.md).
[attest](https://github.com/fredericrous/attest) — the CI end of the same
story: amont signs a note at pre-push naming the gates that really ran, and
attest verifies it so CI can skip them. No connection to this guard beyond
the author and the conviction all three share — trust what was verified,
never what was reported. The masked push at the top of this page is what
that looks like when it fails.
## License
[MIT](LICENSE).