The trust model

A repository you clone cannot run code on your machine until you say it may.

That is the whole claim. This page is how it is enforced and where it stops.

The problem it solves

A repository can declare checks of its own in a committed amont.conf, and those declarations are shell commands. Committing the file is the point — it is how a team shares a check.

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. Reviewing a diff before running it is such a decision. Nothing asked for that.

This matters most for people who set init.templateDir (see install), because for them the hooks are already present in every repository they clone — including one cloned only to read.

What happens instead

A manifest is inert until trusted. Its declared checks are listed by amont list with the reason they will not fire:

declared in an untrusted amont.conf — review it, then `amont trust`
amont trust          # show what this repo declares, and accept it
amont trust --show   # what is trusted here
amont trust --revoke # forget it

amont trust prints the declarations before asking. That is not politeness: "trust this file" is not a question anybody can answer without seeing it, and a prompt that does not show the file is a prompt that trains people to press y.

amont trust outside a repository is refused rather than falling back to .. Trust is recorded per repository, keyed by the root that resolves — so a . fallback would let amont trust in ~ read ~/amont.conf, show its declarations, and record trust against a repository that does not exist, in a state no later --revoke would find.

The record is a fingerprint of the file, stored in --local git config under amont.trusted — local, never committed, so a repository cannot declare itself trusted.

The key is multi-valued: it holds every manifest you have accepted here, most recent last, capped at sixteen. --local config is shared by all of a repository's worktrees, so a single value made them fight — accepting one checkout's manifest reported every other checkout on a different branch as TRUSTED ONCE, AND CHANGED SINCE and stopped its declared checks, until somebody re-accepted there and broke the first one. With a worktree per task the record never settled. Since consent is keyed on content rather than on place, a set is the honest shape: two worktrees with the same amont.conf need one acceptance between them.

One consequence, stated rather than buried: reverting a manifest to bytes you accepted earlier no longer asks again. Those bytes were reviewed, and the cap bounds how far back that reaches — but it is a weaker guarantee than a single value gave.

Because it is keyed on content, a git pull that adds a command does not inherit the consent given to the file before it. That state is reported distinctly from "never trusted", because somebody changed it is a different thing to tell a reader than you have not looked at this yet:

amont.conf changed since it was trusted — review it, then `amont trust`

That is a gap, not a block, when the change arrived from somewhere else: the checks it declares show as "could not run" and your commit proceeds, because a command that never ran has judged nothing. It is a block when the commit at hand is the one changing amont.conf (pre-commit-manifest-trust). You are the author of that content, and without it every check the file declares would stand down for exactly the commit that introduces them — letting everything else in that commit through ungated. The check names the fix and trusts nothing itself; accepting is still amont trust, and still yours. It stays quiet during a merge, rebase, cherry-pick or revert, where a manifest arriving from another branch is the pulled case. Downgrade it like any check: git config amont.severity.pre-commit-manifest-trust warn.

Why git hash-object --no-filters

amont links no external crates (and CI enforces that), and the only hash in std is DefaultHasher — SipHash with a fixed key, not collision-resistant, so a crafted manifest could be made to match a trusted one's fingerprint. Hand-writing SHA-256 is a hundred lines nobody would review as carefully as they should.

git is already a hard dependency of every path in this binary, and git hash-object is the identity git itself uses for content. It is SHA-1 (or SHA-256 in a repository configured for it) — not a guarantee against a determined attacker with a chosen-prefix collision, but enormously better than SipHash, it costs no dependency, and you can reproduce it by hand to check what you trusted:

git hash-object --no-filters amont.conf
git config --local --get-all amont.trusted   # every manifest accepted here

--no-filters is the load-bearing flag. Without it, git applies the clean filter and eol conversion that the repository's own committed .gitattributes asks for — so the repository would be choosing the transform its consent is taken through, and two manifests the parser reads differently could be given the same id. Consent is bound to the bytes that are parsed.

For the same reason, where a caller already holds the file's bytes, the state is decided about those bytes rather than by re-opening the path. Two reads of a file somebody is deciding about can disagree, and the decision would then be recorded about bytes nobody was shown.

Two windows that were closed, and how

Between showing and answering. amont install prints the manifest, then blocks on a keypress — sometimes for several seconds — before recording anything. Re-hashing at that point would trust whatever is on disk then, which is not necessarily what was shown. So callers fingerprint what they show before asking, and pass that same value back to be verified again once the answer is in. If the file changed in the window, nothing is trusted and the prompt says so.

Concealing a declaration inside the listing. Every field in that listing is repository-controlled text, and it is the text somebody is about to say yes to. It is sanitised before the column padding is computed: a terminal escape sequence is zero columns wide, so it would silently shift the alignment even if it did nothing worse — and a repository that can move the rendering can hide a line from the person consenting to it. A repository must not be able to pick how its own consent is rendered any more than it can pick how it is hashed.

A vendored pack is consented to like anything else

amont add (custom checks) copies declarations from somebody else's repository into your amont.conf. It grants no trust, and it gets no exemption:

  • the append changes the file's content, so the fingerprint no longer matches and every declared check goes inert — including ones you had already trusted — and pre-commit-manifest-trust blocks the commit that carries the append until you have reviewed it;
  • the pack's rows are shown by amont trust alongside your own, in the same listing, with no marking that would invite skimming past them;
  • editing a vendored row by hand revokes consent exactly as editing a hand-written one does.

The commit id recorded in the block says where the text came from. It is provenance, not authority: it tells you the bytes are the ones that repository published, and says nothing about whether the commands are a good idea. Reading them is still the gate, and it is still yours.

What this does not protect against

Stated plainly, because a security boundary described only by what it stops is a marketing claim:

  • It is not a sandbox. Once you trust a manifest, its commands run with your privileges. Trust is a review gate, not containment.
  • It says nothing about the built-in checks. Those are code in the binary you installed, and are governed by hook.skip and severity, not by trust.
  • It does not protect a repository you wrote the manifest in. Your own amont.conf is trusted by you, once.
  • SHA-1 is the floor, in a repository using git's default object format. See above for why that trade was taken.

To report something this model gets wrong, see SECURITY.md.