|
| 1 | +# Hooks: the agent gate and the git pre-commit hook |
| 2 | + |
| 3 | +`cargo fallow hooks` installs a check that runs before a commit. The check |
| 4 | +blocks a commit only when `cargo fallow check` finds a new finding in the |
| 5 | +changed files. A finding that the base commit already has does not block. An |
| 6 | +error of the tool does not block. |
| 7 | + |
| 8 | +```bash |
| 9 | +cargo fallow hooks install --target agent # Claude Code and Codex |
| 10 | +cargo fallow hooks install --target git # the git pre-commit hook |
| 11 | +cargo fallow hooks status |
| 12 | +cargo fallow hooks uninstall --target agent |
| 13 | +``` |
| 14 | + |
| 15 | +| Flag | Meaning | |
| 16 | +| --- | --- | |
| 17 | +| `--target agent\|git` | The surface. `agent` is the gate of Claude Code and Codex. `git` is the pre-commit hook. | |
| 18 | +| `--agent claude\|codex` | Only this harness of the agent target. The default is each harness whose folder (`.claude/` or `.codex/`) exists, else Claude Code. | |
| 19 | +| `--manifest-path <PATH>` | The `Cargo.toml` of the workspace. | |
| 20 | +| `--dry-run` | Show the changes, and write nothing. | |
| 21 | +| `--force` | Replace or remove a script that someone edited, or a hook that cargo-fallow did not write. | |
| 22 | +| `--format human\|json` | The output format. The JSON documents are `hooks` and `hooks-status`. | |
| 23 | + |
| 24 | +The commands write to the git top level of the workspace. When the workspace |
| 25 | +is in a subfolder, the scripts record the path of its `Cargo.toml`. |
| 26 | + |
| 27 | +## The agent gate |
| 28 | + |
| 29 | +The gate is a `PreToolUse` hook on the shell tool of the harness: |
| 30 | + |
| 31 | +| Harness | Configuration | Script | |
| 32 | +| --- | --- | --- | |
| 33 | +| Claude Code | `.claude/settings.json`, matcher `Bash`, timeout 300 seconds | `.claude/hooks/cargo-fallow-gate.sh` | |
| 34 | +| Codex | `.codex/hooks.json`, matcher `^Bash$` | `.codex/hooks/cargo-fallow-gate.sh` | |
| 35 | + |
| 36 | +The install keeps each other key and each other handler in the configuration. |
| 37 | +The handler of the fallow gate for JavaScript (`fallow-gate.sh`) stays, so the |
| 38 | +two gates can run in one repository. |
| 39 | + |
| 40 | +For each shell command of the agent, the gate does these steps: |
| 41 | + |
| 42 | +1. It skips each command that is not a `git commit` or a `git push`. A |
| 43 | + command without the words `git` and `commit` or `push` starts no process. |
| 44 | +2. It finds the workspace of the session folder, and of each folder that the |
| 45 | + command names (`cd`, `git -C`, `--work-tree`, `--git-dir`). A named folder |
| 46 | + counts only when it is in the same repository: in the work tree of the |
| 47 | + session, or in a linked worktree. The check runs `cargo metadata` in the |
| 48 | + folder, so a folder of another repository is not checked. |
| 49 | +3. It runs `cargo fallow check --fail-on-issues --offline` on the files that |
| 50 | + changed since the base: |
| 51 | + - A commit compares with `HEAD`. Staged, unstaged and untracked changes |
| 52 | + count. |
| 53 | + - A push compares with the upstream of the branch, else with |
| 54 | + `origin/HEAD`, `origin/main`, `origin/master`, `main` or `master`. A push |
| 55 | + also runs the `file-growth` gate of `size` (`--base`). |
| 56 | +4. It runs the same check on the base commit, and leaves out each finding |
| 57 | + that the base commit has. The result of the base commit stays as a cache |
| 58 | + in the git folder (`.git/cargo-fallow-gate/`), so a retry after a block |
| 59 | + runs one check. A commit cannot add a file there. |
| 60 | +5. When an enforced gate fails, it blocks the command (exit code 2), and |
| 61 | + prints one line for each new finding, the failed gates and the path of the |
| 62 | + report. |
| 63 | + |
| 64 | +```text |
| 65 | +cargo-fallow-gate: blocked by cargo-fallow 0.2.0 in /work/ripgrep |
| 66 | +cargo-fallow-gate: failed gates: error-severity-findings |
| 67 | +cargo-fallow-gate: 1 finding(s) in the changed files caused the block: |
| 68 | + deps/unused-dependency crates/printer/Cargo.toml:22 Remove `globset` from [dependencies]. |
| 69 | +cargo-fallow-gate: full report: /work/ripgrep/.fallow/cargo-gate-report.json |
| 70 | +cargo-fallow-gate: fix the findings and try again. `cargo fallow explain <rule>` explains a rule, and `cargo fallow fix --dry-run --format json` lists the fixable actions. |
| 71 | +``` |
| 72 | + |
| 73 | +The gate keeps the commit open, with one line on stderr, in these cases: |
| 74 | + |
| 75 | +- The binary is not found (`CARGO_FALLOW_GATE_BIN`, `PATH`, then |
| 76 | + `${CARGO_HOME:-~/.cargo}/bin`). |
| 77 | +- The payload is not valid JSON. |
| 78 | +- `cargo metadata` fails, for example because `--offline` needs a download. |
| 79 | +- The repository has no commit, or a push has no base. |
| 80 | +- The runtime crashes or stops on a signal. |
| 81 | + |
| 82 | +The gate needs bash. On Windows, use Git Bash or WSL. |
| 83 | + |
| 84 | +On a workspace of about 1800 `.rs` files, a release build took 34 seconds for |
| 85 | +the first check of a commit (the base commit and the work tree), and 17 |
| 86 | +seconds for a retry with the cache. The Claude Code handler has a timeout of |
| 87 | +300 seconds. A timeout keeps the commit open. |
| 88 | + |
| 89 | +### Settings |
| 90 | + |
| 91 | +| Variable | Meaning | |
| 92 | +| --- | --- | |
| 93 | +| `CARGO_FALLOW_GATE_BIN` | The `cargo-fallow` binary to run. | |
| 94 | +| `CARGO_FALLOW_GATE_MIN_VERSION` | The lowest version that the gate accepts. The default is the version that installed the gate. Below the floor, the gate blocks and prints the upgrade command. An empty value turns the floor off. | |
| 95 | +| `CARGO_FALLOW_GATE_REPORT` | The path of the JSON report. The default is `.fallow/cargo-gate-report.json` in the workspace root. A relative path is relative to the workspace root. | |
| 96 | +| `CARGO_FALLOW_GATE_DEBUG=1` | Also log each command that the gate skips. | |
| 97 | + |
| 98 | +The report is the JSON report of `check`. The gate never writes through a |
| 99 | +symbolic link: when the report path or one of its folders is a link, the gate |
| 100 | +writes the report to a temporary file and names that file. The gate writes |
| 101 | +`.fallow/.gitignore` with `*`, so the report does not make the work tree |
| 102 | +dirty. |
| 103 | + |
| 104 | +### Edited scripts |
| 105 | + |
| 106 | +Each generated script holds a checksum. `hooks install` replaces a script |
| 107 | +whose checksum matches. A script that someone edited stays: `install` exits 2, |
| 108 | +and `uninstall` keeps it. Give `--force` to replace or remove it. |
| 109 | + |
| 110 | +## The git pre-commit hook |
| 111 | + |
| 112 | +`--target git` writes the `pre-commit` hook in the hooks folder that git uses |
| 113 | +(`git rev-parse --git-path hooks`), so `core.hooksPath` and worktrees work. |
| 114 | +With a `.husky/` folder, the hook is `.husky/pre-commit`. When `hk.pkl`, |
| 115 | +`lefthook.yml` or `.pre-commit-config.yaml` exists, the command writes no file |
| 116 | +and prints the command to add to the pre-commit step of that tool. |
| 117 | + |
| 118 | +The hook runs the same commit check as the agent gate. It blocks only on new |
| 119 | +findings, and an error of the tool does not block. Skip it for one commit with |
| 120 | +`git commit --no-verify`. |
| 121 | + |
| 122 | +## Status |
| 123 | + |
| 124 | +`cargo fallow hooks status` gives the state of `claude`, `codex` and `git`: |
| 125 | + |
| 126 | +| State | Meaning | |
| 127 | +| --- | --- | |
| 128 | +| `installed` | The handler and the generated script are in place. | |
| 129 | +| `partial` | The handler or the script is missing. | |
| 130 | +| `edited` | The script is not the generated one. | |
| 131 | +| `managed` | A hook manager runs the git hooks. | |
| 132 | +| `absent` | Nothing is installed. | |
0 commit comments