amont-agent 2.14.0

A guard that inspects a shell command before Claude Code runs it
amont-agent-2.14.0 is not a library.

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.

CI License

$ amont-agent check 'git push origin main 2>&1 | tail -5'
pipe-to-tail [deny]
  `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.
  ▸ git push origin main 2>&1 | tail -5

Install

curl -fsSL https://raw.githubusercontent.com/fredericrous/amont-agent/main/install/install.sh | 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:

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

stance effect
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:

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.

The rules

rule ships as what it catches
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 rehearse --wait
foreground-poll advise a polling loop or gh run watch in the foreground, where the tool's ten-minute clock will kill it one poll short
sed-in-place advise sed -i spelled for the other sed (-i '' on GNU, bare -i on BSD)
kubectl-gitops advise an imperative kubectl write in a repository Flux or Argo reconciles
tag-after-commit advise git tag chained onto a git commit that a hook may have refused
worktree-remove-force advise git worktree remove --force on a worktree that still holds uncommitted work
amend-pushed advise git commit --amend on a commit the remote already has
branch-force-delete observe git branch -D on a branch whose commits are on no remote and not merged
worktree-isolation observe a branch created, or git reset --hard, in the primary checkout of a repository that already has linked worktrees
stdin-hang observe a command that will read standard input, with nothing on it — cat > file, a bare interpreter, tee outside a pipe — which blocks silently until the tool's clock runs out
glob-in-flag-value advise an unquoted glob inside a flag value (--include=*.ts), which zsh expands — or fails on — before the program sees it
glob-no-match advise an unquoted glob operand that matches nothing, which under zsh aborts the clause before it starts while a later clause reports success
equals-separator observe a bare word beginning with = (echo ===, [ x == y ]), which zsh reads as a command lookup and whose failure aborts the whole command list
path-operand-missing advise a read of a path that is not there — under the grep shim a warning in mid-stream, for the coreutils one line and carry on — which the chain then reports as success
stat-bsd-format advise stat -f '%…' on GNU stat (or -c on BSD), which prints a filesystem report where a timestamp was wanted
whole-file-dump advise a file poured whole into the tool result by cat/sed -n/head — 31% of all result bytes measured — where the Read tool would have windowed it
file-reread advise a Read, or a cat, of a file this session already has in context and that is unchanged on disk since — answered from the session's own record, not the command
persisted-output-dump advise reading back whole a tool result the harness saved to a file for being too large, paying for it twice

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.

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.28, amont rehearse --wait runs the same gate on a snapshot of HEAD with no connection open — or follows the rehearsal amont.rehearseOnCommit already started — and stamps the tree, so the push that follows skips the suite (amont run pre-push on 1.27). 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.

The seven rules added in 2.2.0 came out of the transcripts the same way — nineteen thousand Bash calls, sorted by what failed, was killed, or drew a correction. Each fires on shape and, where the fact lives in the world, confirms it first: whether the call already runs in the background, which sed is on PATH, whether the repository holds a Flux or Argo resource, whether the worktree is dirty, whether the remote has the commit, whether any other branch has the commits. The one shape-only rule, tag-after-commit, names a failure every command in the chain reports as success.

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, unbalanced quotes, eval — opaque never fires. The unit is the PIPELINE: git status -s | xargs git add && git commit … | tail -1 has one run we cannot read and one we can, and the second is still judged. eval, source and . are the exception — they run in this shell and can move it, so they hide the whole line.
  • 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.
  • No repository can change a stance. Stances are read from --global and --system git config only — never from a committed file, and never from the .git/config of the repository the agent is standing in, which is a file that agent could write.

The reasoning in full.

Turning it down

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 · Stances · The rules · Measuring and graduating · The session notice · Configuration · What it will not do

Contributing

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 — 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.

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.