OpenTuple
Learns your repository's conventions from its code, and holds AI coding agents to them.

Every codebase has rules nobody wrote down: this package always closes rows
and checks rows.Err(), that one logs through slog, receivers of Store
are always s, tests here use testify. AI agents break them constantly,
because they cannot see them.
OpenTuple finds those rules by reading the code — only rules that at least
98% of a directory already follows — and checks every edit an agent makes
against them. When an edit breaks one, the agent is told, with the
evidence, and fixes it. One Go binary, runs locally, no configuration.
What the agent sees
From a real run (Claude Code, headless, Claude Haiku). The agent was asked
to add a summary printer to a CLI package. Its first version printed with a
bare fmt.Fprintf. Every other printer in that package styles its output
through the theme, so the hook answered:
internal/cli/summary.go:9: PrintIndexSummary calls fmt.Fprintf but never Render
Rule: In internal/cli, functions that call fmt.Fprintf also call Render. [helper_pairing]
Evidence: 12 of 12 functions in internal/cli that call fmt.Fprintf also call Render (100%); 4% of the functions that do not call fmt.Fprintf call Render.
Example: reportWrittenFiles (internal/cli/generate.go:218) follows this rule.
The agent's next edit wrapped the text in the package's style
(StyleDim.Render(...)), the check passed, and the session ended. Nobody
told it about the theme; the repository did.
Quickstart
go install github.com/CtrlAltDelete-Ops/opentuple/cmd/opentuple@latest
cd your-repo
opentuple init .
Then start a new Claude Code session in the repository. init does three
things:
- Indexes the repository into
~/.opentuple/repos/ (Kubernetes, 9,711
files: 27 seconds on an Apple M3).
- Writes convention files agents read at session start: a
CLAUDE.md
per directory that has rules, and one flat AGENTS.md for tools that read
a single file.
- Installs the check hook in
.claude/settings.local.json. That is
Claude Code's personal settings file: the hook names your binary and
your checkout by absolute path, so it must not be committed, and init
makes sure git ignores it. Each teammate runs opentuple init . once.
Re-run opentuple init . after large changes; re-indexing is idempotent.
Not on Claude Code? opentuple init . --git-hook adds a pre-commit hook
that runs the same check on staged files, and AGENTS.md works with Codex,
Cursor and other tools that read one.
Already have a CLAUDE.md? OpenTuple never edits a file it did not
write. It puts its rules in CLAUDE.opentuple.md beside yours; add the line
@CLAUDE.opentuple.md to your CLAUDE.md to import them.
What it enforces
Rules are mined per directory, with test files as their own scope, so a
convention of your tests is never imposed on your library code.
| Rule type |
Example of what gets flagged |
helper_pairing |
calls QueryContext but never Err, where every function here that queries also checks rows.Err() |
family_dominance |
calls log.Printf, but logging in pkg/svc goes through slog |
wrapper |
calls text.Pluralize directly, where 23 callers go through the project's Pluralize |
receiver_name |
names a Client receiver cl, where 63 of 63 methods use c |
doc_comment |
an exported function without a doc comment, or one that doesn't start with its name |
error_handling |
swallows an error where this package wraps or returns every one |
deferred_call |
calls reg.Verify(t) without defer, where every test here defers it |
test_helper |
a test helper that never calls t.Helper() |
file_header |
a new file without the license header nearly every file carries |
helper_absence |
calls panic, console.log, eval, print… where no function here does |
Opt-in, because they are useful in some repositories and noisy in others:
--io (a function doing I/O in a package that never does), --duplicates
(a copy of an existing function, identifiers renamed), and --absences
(any helper or import the directory never uses, not only banned builtins).
Silence is a feature. A rule exists only where at least 98% of a
directory's functions follow it, with at least 10 of them, and the evidence
is clear-cut: a pairing must beat the paired helper's base rate by 30
points, an absence needs enough functions that would be expected to call
it. A false alarm costs more than a missed one, because the first false
alarm teaches people to turn the check off. On a small or inconsistent
repository OpenTuple may find nothing to enforce, and then it says nothing.
Does it work?
It stays quiet on code that follows its own conventions. opentuple check --all audits a repository against the conventions mined from it. By
construction up to 2% of a rule's own directory may deviate, so this is the
noise floor a user would see on unchanged code:
| Repository |
Files checked |
Flags |
| junegunn/fzf |
79 |
0 |
| cli/cli (gh) |
860 |
3 |
| kubernetes v1.29.0 |
7,845 |
49 |
| 10 JS/TS repos (hono, express, koa, fastify, axios, zod, zustand, immer, preact, trpc) |
3,191 |
0 |
The 49 in Kubernetes are minority deviations: 31 test functions using a
different assertion library from the rest of their package, 11 doc
comments, 3 receiver names, 2 pairings, 1 logging call outside klog, 1
error-handling site. A 29-repository sweep across six languages in October
left 22 flags in total, each a minority deviation or a deliberate
exception.
When an agent slips, the hook catches it. With the hook off, agents
broke one of the repository's conventions in 11% of runs. With the hook
on, every violation it caught was fixed on the agent's next edit.
Telling agents the rules up front did not. In July we tested handing
the agent the mined conventions just before each edit (the design this
project started with; 6 blind-scored runs on Kubernetes with Claude Opus):
it wrote the same code with or without them. That result is why the check
exists. The convention files generate writes are the same kind of
up-front guidance and have not been shown to help on their own; they are
still written because they cost nothing and some tools have no hook.
Commands
| Command |
What it does |
opentuple init <path> |
index if needed, write convention files, install the Claude Code hook (--no-generate, --no-hook, --git-hook, --dry-run, --reindex) |
opentuple index <path> |
index or re-index (--mode fast|balanced|quiet) |
opentuple generate <path> |
write the convention files only (--format claude|agents|both, --output <dir>, --dry-run) |
opentuple check <file>... |
check files against their directories' conventions (--staged, --all, --hook) |
opentuple rules <path> |
list the rules check enforces on a file or directory, with the evidence |
opentuple status <path> |
show what is indexed |
check exits 0 when clean, 1 on violations, 2 on an error. With --hook
it exits 2 on violations and 1 on an error, because Claude Code shows a
hook's stderr to the agent only on exit 2. Every command writes to stderr
and leaves stdout empty, so OpenTuple is safe inside pipes and hooks.
Configuration
Nothing is required. To tune the check, add .opentuple.json at the
repository root:
{
"min_confidence": 0.99,
"disable": ["doc_comment"],
"enable": ["io_boundary"],
"ignore_paths": ["vendor", "*.pb.go"]
}
The convention files follow the same settings, so they never state a rule
the check would not hold an agent to. To waive rules for one function, put
// opentuple:ignore (all rules) or // opentuple:ignore receiver_name
(one type) in its doc comment; // opentuple:ignore-file near the top of a
file skips the file.
How it works
A concurrent pipeline — gitignore-aware walker, a tree-sitter parser pool,
one batched SQLite writer — extracts every function with its calls, a
behavioral fingerprint, and per-file facts (imports, header). Generated
files are skipped. A miner then finds, per directory and per test
partition, what nearly every function does: which helpers pair, which
member of an API family wins, what receivers are named, how errors are
handled. check re-parses only the edited file with the same extractor
and compares it with the rules of its directory, in tens of milliseconds
(0.08 s for a 3,000-line file against the 52,000-function Kubernetes
index).
Output is deterministic: the same repository produces byte-identical
rules on every run.
Languages and limits
Go, Python, JavaScript, TypeScript, Rust and C, via tree-sitter. Go has the
deepest coverage (receivers, test helpers, error wrapping). In JavaScript
and TypeScript, functions are found wherever they are declared or bound
to a name (const f = () => {}, exports.f = function () {}, class
fields); callbacks passed inline are part of the function around them.
.tsx files are parsed with the TypeScript grammar, which has no JSX, so
JSX-heavy components are only partly seen. Re-indexing is not incremental
yet: re-run init or index after large changes.
Everything runs on your machine. OpenTuple makes no network requests; the
index lives in ~/.opentuple/repos/ and can be deleted at any time.
License
Apache-2.0