Skip to main content

🪝 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 trust before 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: and github: entries do name commands, and they run only after a human runs gaff init --git or gaff init --github in that repo. Note the limit of that claim. A handler's command still inherits the working directory gaff was called in, and tools like git, make, and just read executable settings from there. Handlers are therefore deny-by-default, and they need gaff trust in 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

gaffr-0.1.4.tar.gz (198.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

gaffr-0.1.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl (2.8 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64musllinux: musl 1.1+ x86-64

gaffr-0.1.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl (2.6 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64musllinux: musl 1.1+ ARM64

gaffr-0.1.4-py3-none-macosx_11_0_arm64.whl (2.5 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

gaffr-0.1.4-py3-none-macosx_10_12_x86_64.whl (2.6 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

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

Hashes for gaffr-0.1.4.tar.gz
Algorithm Hash digest
SHA256 a725d6d69ca4533a582e0ac9463af8a1f4d01b6a47341af86eb8ad64324606c3
MD5 3e26bcdb10eb451eb0e76894708dec7d
BLAKE2b-256 f09ec026d006770d6fbdfea548f781522afe0de46cf90217698f7b0367f9ec5c

See more details on using hashes here.

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

Hashes for gaffr-0.1.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl
Algorithm Hash digest
SHA256 c2a33bb84c672e356170aea9980791b480e09657f16315f261e6c66ca3f052f5
MD5 625f677f1fcd3aa3286ecedcddc810a8
BLAKE2b-256 73dfab172ab5d95bfe7b3bf77dc29da1cf8f366941d39c36e5cb804eacb4cf35

See more details on using hashes here.

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

Hashes for gaffr-0.1.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl
Algorithm Hash digest
SHA256 930afb5f13628b1d80a9934f61e6510818a2b0d55fc3d8f5e2793915e20f761b
MD5 72b9fedad9af0f014773a9a22f4e8c5f
BLAKE2b-256 d9e397945d709f0b5aad92e46080dd7cf1dfa7761fcc0f7862bacc91bd86e694

See more details on using hashes here.

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

Hashes for gaffr-0.1.4-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 9b3589e4669831b3b136afa6299366b7ab61e7b85da3158db76ebf6d4c1f41f9
MD5 04807404f82ff2d434839c45809b8ebd
BLAKE2b-256 79343488ed355fdf82c0464770fd7b3ed036d540587d397f406022b826d3b035

See more details on using hashes here.

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

Hashes for gaffr-0.1.4-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 5480a2b60da038a76549a105e173b1c3d3e2ba44f318ad58f270f4bc702ae94f
MD5 7b10f1537dba18e98be5a5811976cd4a
BLAKE2b-256 e8a80fc483fc48aa68d4ad79f8ce579ee9b2029e88ae8c2ac95046d446d0f205

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.4 This release

5 files

0.1.3

5 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page