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.3.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.3-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.3-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.3-py3-none-macosx_11_0_arm64.whl (2.5 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

gaffr-0.1.3-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.3.tar.gz.

File metadata

  • Download URL: gaffr-0.1.3.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.3.tar.gz
Algorithm Hash digest
SHA256 3407fa0c352f5f4ba16d139578a0ab87879792b03486d695fe35b16e2b78f4bb
MD5 c74e11fd39ef77dad1c0525e03dc15d5
BLAKE2b-256 e2dcb32d305a1b83a441ed289246fe1f88871c1130cc20a30d0c3dd0be282a2f

See more details on using hashes here.

File details

Details for the file gaffr-0.1.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl.

File metadata

  • Download URL: gaffr-0.1.3-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.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl
Algorithm Hash digest
SHA256 3a5b35cbc3bbdb933c7ca9197c480a3c3be0637cbb5d91ca37545cd6bb1476c8
MD5 a0720865add15d2a99e7407765c9ee2b
BLAKE2b-256 40d2bfeb8b25e9098ccb183e9870d79f66c9ad851049dc94168eabe0dc66fe13

See more details on using hashes here.

File details

Details for the file gaffr-0.1.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl.

File metadata

  • Download URL: gaffr-0.1.3-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.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl
Algorithm Hash digest
SHA256 cd597c016d620c4922bf536a7bce221a37cbd1764d2dae43d73057cc0e31d6b5
MD5 b09c454748c6c13f3d49256add8d0f89
BLAKE2b-256 1c88b7f5a7278d58bd6861e8569955298684bd157b379499f3f38615798119fe

See more details on using hashes here.

File details

Details for the file gaffr-0.1.3-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: gaffr-0.1.3-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.3-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c7c762319cd5fc81f134dc39f2a6b25da3bfc18771a5981f4c14db448dadfb5b
MD5 3e1effe28e4bbcb5da423e91667bd74e
BLAKE2b-256 6266c9643d2054ea42f9de9a4442d2605e1720210412bfc50e24d1844734b119

See more details on using hashes here.

File details

Details for the file gaffr-0.1.3-py3-none-macosx_10_12_x86_64.whl.

File metadata

  • Download URL: gaffr-0.1.3-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.3-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 9f5694444463ead88e3fc8dfdeab13fac3f878339c977d1dbacda9b750415ffe
MD5 6dc4683b6811cd6ec509dd1a65632ba2
BLAKE2b-256 b64fea3f290d9b243f454dcd245d3ee36b989e11d21a9ea1e6b2c0b3c75e021f

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.4

5 files

This release

0.1.3 This release

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