🪝 gaff
A gaff is a pole with a sharp hook on the end. It lands what's drifting past.
gaff is a context-lifecycle handler for coding agents. It counts the hook events of a session and re-injects context on a cadence. It also delivers prime sections and advisory profiles.
The problem
Context injected at session start decays as the conversation grows. It moves into the low-attention middle of the context window. The decay follows the number of messages and tool calls, not the wall clock.
The agent harness re-delivers no context on a cadence. Rules and skills load on conditions. Reminders do not exist. A session of 300 tool calls ends with its opening instructions effectively invisible.
What gaff does
- Counters. gaff tallies the prompts and the tool calls of each session in an append-only ledger. A tool call counts once across its Pre, Post, and failure events.
- Cadences. A reminder re-injects its text every N tool calls or prompts. An agent can also schedule a one-shot reminder N tool calls into its own future, and gaff re-arms it after a context compaction.
- Prime sections. The session-start context, split into sections. Each section refreshes on its own cadence.
- Handlers. An external command whose output becomes context, on a
cadence. Handlers live only in the user-scoped config, and a directory
must be trusted with
gaff trustbefore any command runs in it. - Guards. A guard refuses a tool call that matches a regular expression. Declare one at user level and it applies in every repo. This is the feature that blocks, and it blocks on purpose.
- Git hooks. gaff writes the scripts in
.git/hooks/, and they call back into gaff. One config declares the agent side and the git side. A hook gaff did not write is kept and called first. - GitHub workflows. gaff generates them from the same config and checks them for drift. A check declared once runs in the git hook and in CI.
- Profiles. A profile is a named overlay that selects which entries are active and overrides their cadences. A transition policy states which profiles an agent may select for itself. Profiles are advisory, and gaff blocks nothing through them.
Where config lives
$HOME/.config/gaff/gaff.yml holds what you want in every repo.
.gaff/gaff.yml holds what belongs to one repo. A repo entry under a
name the user already declared is refused with a warning, and the
user's entry stands. A repo never widens the profiles an agent may
grant itself.
Handlers live only in $HOME/.config/gaff/handlers.yml.
What gaff is not
- Not an agent-hook dispatcher. The harness's own hook system owns the matching, the timeouts, and the parallelism there. gaff registers as one handler. gaff does dispatch its own git hooks, because git has no dispatcher of its own.
- Not an enforcement layer. gaff refuses a tool call through a guard and a stop through a hold, and nothing else. It injects context only on the events whose output channel is the model's session framing. It never decorates a tool result.
- Not a way to run repo-declared code from a hook. On the agent
path the repo-level config is data: sections, text, and cadences. A
handler's command can only be declared in the user-scoped config. A
repo's
git:andgithub:entries do name commands, and they run only after a human runsgaff init --gitorgaff init --githubin that repo. Note the limit of that claim. A handler's command still inherits the working directory gaff was called in, and tools likegit,make, andjustread executable settings from there. Handlers are therefore deny-by-default, and they needgaff trustin each directory the agent starts in.
Install
The package is gaffr on PyPI and npm, because gaff was taken. The
command is gaff everywhere, and both names install together.
cargo install --locked gaff
brew install cjohnhanson/tap/gaff
uv tool install gaffr
npm install -g gaffr
cargo install builds from source. It needs Rust 1.88 and a C
compiler. The other three carry a prebuilt binary for macOS and Linux,
x86-64 and arm64, published by a tagged release.
To build the unreleased main branch:
cargo install --locked --git https://github.com/cjohnhanson/gaff
Or run it without installing:
uvx gaffr status
npx gaffr status
A release also carries prebuilt archives and a .deb, on the releases
page. Each archive holds
the binary and the man page. Install a .deb with dpkg -i: it is a
file, not a repository, so apt-get install does not reach it.
Check the install with gaff --version, and gaff doctor for what is
live in a clone.
Using it
gaff init # register the hooks in the host's settings file
gaff remind "check CI" --after 10 # one-shot, N tool calls into the future
gaff status --session <id> # counters, pending entries, one-shots
gaff check # validate .gaff/gaff.yml
gaff doctor # what is live in this clone
gaff init --git # write the git hook scripts
gaff init --github # generate the workflows
gaff check --github # report a workflow that drifted
gaff trust # allow handlers in this directory
gaff check --handlers # validate ~/.config/gaff/handlers.yml
gaff profile list # the declared profiles and who may set them
gaff profile set focus # switch, and re-prime the sections
gaff log # what gaff injected into this session
gaff docs getting-started # the bundled documentation
Status
Every feature and every command listed above is built and runs. The version is below 1.0, so the config keys and the output formats can still change.
Two host adapters ship: Claude Code, and generic, which reads gaff's
own normalized field names for a host that speaks them. A host declares its
payload mapping, its event names, and its settings path in
src/adapter.rs, and nothing else in gaff changes. gaff ships no
guessed schema for a host nobody has tested.
A missouri state-graph suite of 28 paths and the cargo unit tests cover
this. The suite's error-surface path checks that a gaff failure exits 0
or 1, never the blocking code 2. Exit 2 belongs to a guard that refuses
a tool call, a stop hook that refuses a stop, gaff run reporting an
agent's refusal, and gaff githook relaying the failing command's own
code.
Related
- tisket — issue tracker. Markdown issues with YAML frontmatter, in the repository
- zettel — zettelkasten notes for a repository
- almanac — agent skill index, over pluggable sources
- missouri — end-to-end tests as directed graphs of filesystem states
- mdstore — the frontmattered markdown library the other three store documents with
- kersh — the declarative agent runner a hook agent calls by default
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gaffr-0.1.4.tar.gz.
File metadata
- Download URL: gaffr-0.1.4.tar.gz
- Upload date:
- Size: 198.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a725d6d69ca4533a582e0ac9463af8a1f4d01b6a47341af86eb8ad64324606c3
|
|
| MD5 |
3e26bcdb10eb451eb0e76894708dec7d
|
|
| BLAKE2b-256 |
f09ec026d006770d6fbdfea548f781522afe0de46cf90217698f7b0367f9ec5c
|
File details
Details for the file gaffr-0.1.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl.
File metadata
- Download URL: gaffr-0.1.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl
- Upload date:
- Size: 2.8 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64, musllinux: musl 1.1+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2a33bb84c672e356170aea9980791b480e09657f16315f261e6c66ca3f052f5
|
|
| MD5 |
625f677f1fcd3aa3286ecedcddc810a8
|
|
| BLAKE2b-256 |
73dfab172ab5d95bfe7b3bf77dc29da1cf8f366941d39c36e5cb804eacb4cf35
|
File details
Details for the file gaffr-0.1.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl.
File metadata
- Download URL: gaffr-0.1.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl
- Upload date:
- Size: 2.6 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64, musllinux: musl 1.1+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
930afb5f13628b1d80a9934f61e6510818a2b0d55fc3d8f5e2793915e20f761b
|
|
| MD5 |
72b9fedad9af0f014773a9a22f4e8c5f
|
|
| BLAKE2b-256 |
d9e397945d709f0b5aad92e46080dd7cf1dfa7761fcc0f7862bacc91bd86e694
|
File details
Details for the file gaffr-0.1.4-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: gaffr-0.1.4-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.5 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b3589e4669831b3b136afa6299366b7ab61e7b85da3158db76ebf6d4c1f41f9
|
|
| MD5 |
04807404f82ff2d434839c45809b8ebd
|
|
| BLAKE2b-256 |
79343488ed355fdf82c0464770fd7b3ed036d540587d397f406022b826d3b035
|
File details
Details for the file gaffr-0.1.4-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: gaffr-0.1.4-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 2.6 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5480a2b60da038a76549a105e173b1c3d3e2ba44f318ad58f270f4bc702ae94f
|
|
| MD5 |
7b10f1537dba18e98be5a5811976cd4a
|
|
| BLAKE2b-256 |
e8a80fc483fc48aa68d4ad79f8ce579ee9b2029e88ae8c2ac95046d446d0f205
|