---
title: "How gaff works"
description: "Why injected context decays, and how counting, arming, and flushing answer it"
type: explanation
canonical_url: https://gaff.eng.rip/how-it-works/
---

# How gaff works

## Count, arm, flush

gaff reads every hook event as JSON on stdin and answers on stdout. Each
event goes through up to three stages:

1. **Count.** An event that carries a counted unit appends one line to
   the per-session append-only ledger. Tool calls deduplicate on
   `tool_use_id`.
2. **Arm.** When a cadence divides the new count, gaff writes a pending
   marker. gaff emits nothing here. The crossing usually happens on
   PostToolUse, whose output channel is unsafe to decorate.
3. **Flush.** The safe events are SessionStart, UserPromptSubmit, Stop,
   and PostToolBatch. At the next one, gaff merges the pending entries
   under the byte cap and emits them as `additionalContext`. gaff
   consumes an entry only when it emits that entry.

## Sessions

The counters are per session. gaff reads the `session_id` from the event
payload. A subagent has its own session and its own counters. `gaff
remind` runs inside a session and resolves the session from `--session`,
then `GAFF_SESSION_ID`, then the host's own variable, which each adapter
names (Claude Code exports `CLAUDE_CODE_SESSION_ID` to a shell
subprocess).

## Compaction

A context compaction erases everything that gaff already delivered. On a
SessionStart whose source is `compact`, gaff re-arms the consumed
one-shots and injects every section again. The ledger survives, and the
cadences continue.

## Failure posture

No gaff failure blocks a session. Every internal failure degrades to a
silent passthrough at exit 0, or to a non-blocking error at exit 1. No
failing path emits the blocking exit code 2, and the tests enforce that.
A degradation is loud. It writes a warning on stderr and a `degraded`
marker in the state directory, and `gaff doctor` reports it.
