Skip to content

Gettier for Agents

The CLI. It runs where your agent runs and where your code is reviewed, and it needs no Gettier server — the gate, the sensors and the registry are all local.

npm install -g gettier

Not published yet

Built and smoke-tested, not on npm yet. From a clone: pnpm --filter gettier build, then run node packages/cli/dist/cli.js.

gettier init <project> [--environment env] [--registry path]
                       [--endpoint url | --local] [--force]
gettier status
gettier sensors list
gettier sensors run <id>
gettier check [--declaration path|-] [--markdown path] [--json]
              [--require-evidence] [--hold-on-unverifiable] [--api-cwd dir]
gettier mcp

Claude Code hooks, Codex configuration and a packaged GitHub Action are not built, and gettier --help says so.

gettier init

Writes .gettier/project.json. It records the name of the environment variable holding your credential (GETTIER_DSN), never its value — the credential never lands in a file.

Flag Meaning
--environment <env> Default development.
--registry <path> Where sensors are read from. Default sensors.yaml.
--endpoint <url> Where telemetry goes.
--local Point at a dashboard on this machine.
--force Overwrite an existing project file.

Resolution order for the endpoint is the project's endpoint, then GETTIER_ENDPOINT, then the hosted API. It never falls back to localhost: this package is published, and a localhost default would send your telemetry to a port on your own machine without telling you.

gettier status reports the project, the registry, whether the credential variable is set, the resolved endpoint and the transport — probing the endpoint and saying whether the credential was accepted.

gettier sensors

gettier sensors list          # id, effects, the categories each one can falsify
gettier sensors run db.identity

Sensors run by id only (architecture rule 6). An id absent from the project's registry is refused, and the refusal names the ids that are allowed. There is no path here that takes a command from a caller — not for debugging, not temporarily.

Registry shape and every field are in Contracts → Contract 2.

gettier check

Reads a declaration, runs the registry sensors that bear on it, and exits. The declaration is Contract 1's declare_lemmas input verbatim — the same object the model produces at the proxy — so an agent writes what it already builds and nothing translates between two shapes that could drift apart.

.gettier/declaration.json     default; --declaration <path> or - for stdin
Exit Meaning
0 the gate allows the change
2 the gate says no — a premise was contradicted, or one nothing could settle
1 the check itself could not run

1 is separate from 2 on purpose. A CI job that cannot tell "the premises did not check out" from "the tool crashed" gets switched off after the second false alarm.

Flag Meaning
--declaration <path> Read from a path, or - for stdin.
--markdown <path> Write a pull-request comment to a file. Posts nothing.
--json Machine-readable result.
--require-evidence Fail a change that measured nothing at all.
--hold-on-unverifiable Restore block-until-verified: coverage gaps block too.
--api-cwd <dir> Where declared symbols resolve against the installed tree. Must be the package that declares the dependencies — under pnpm, a workspace root makes every symbol read as "not installed".

Behaviour worth knowing before you wire it into CI

  • A catch fails the check. Inside a turn a contradiction does not hold the gate — it is fed back and the model replans with the truth in hand. On a pull request there is no replan: the change is already written on top of the premise a measurement disproved.
  • A claim no sensor covers does not fail it. That is the registry having nothing to say, reported so you know which sensor to write next. --require-evidence and --hold-on-unverifiable change that if you want it stricter.
  • A sensor that matched a claim and could not run holds it. "We tried and could not confirm" is not the same as "nothing covers this", and failure must never read as verification. That includes a command that ran and exited non-zero (v0.6.1).
  • Ambient sensors sweep regardless of what the plan says (Contract 2 v0.5), so a premise nobody declared — the database role you connect as, the runtime you deploy onto — is still checked. Those findings are labelled (undeclared, load-bearing).
  • Provenance cannot be earned here. There is no ledger and no conversation, so a lemma tagged measured or told is judged as believed — stricter than the proxy, never looser. The downgrades are reported rather than applied silently.
  • --markdown writes a file and posts nothing. This package holds no GitHub credential and is not going to; the workflow that owns the token decides where the comment goes.

Telemetry from a check carries counts only — decision, how many claims were contradicted, held, verified — never claim text. Running under CI tags the event source: ci rather than agent: a gate held in CI blocked a merge, the same gate held on a laptop informed somebody mid-edit.

A worked CI wiring is in the quickstart.

gettier mcp

Three tools over stdio, for any MCP-capable agent. No Gettier server involved. Register it with your agent as gettier mcp, run from the repository root.

claude mcp add gettier -- gettier mcp
Tool What it does
list_sensors What this repository can measure, and which assumption categories each sensor falsifies. Call it first — a premise nothing covers cannot be checked here.
declare_lemmas The premises the plan rests on, written to .gettier/declaration.json.
check_assumptions Runs the sensors that bear on them; returns contradictions, unresolved claims, and what checked out.

Together they are the loop the proxy runs — ask what is measurable, declare, check, get the contradiction back, replan — driven by the agent rather than intercepted from it. The gate is therefore cooperative here: an agent that never calls the tools is never gated. Use the proxy when the gate has to be unavoidable.

  • declare_lemmas's schema is Contract 1 by reference, not re-expressed in this server's own types — DECLARE_LEMMAS_TOOL.input_schema, passed through. Two copies of a frozen contract drift; a test pins the identity so a version bump cannot leave this behind.
  • No tool accepts a command (rule 6). The registry is readable, the gate is runnable, and there is no third thing.
  • Sensor-derived text comes back inside <<<GETTIER:DATA:…>>> framing (rule 4) — a reading is evidence, never an instruction.
  • check_assumptions returns isError: true when the gate says no. That is a result the model should read and act on, not a broken server.
  • stdout is the protocol channel, so gettier mcp prints nothing to it. Diagnostics go to stderr.
  • No runtime dependency. The transport is written out in src/mcp-stdio.ts: JSON-RPC 2.0, one JSON object per line. @modelcontextprotocol/sdk is a devDependency, imported by one file — the conformance test, which spawns this server as a subprocess and drives it with the reference client. Testing a hand-written protocol implementation against our own reading of the spec would be circular; testing it against the reference is not.

Which clients this is known to work with

Client Evidence
Reference MCP client (@modelcontextprotocol/sdk) Full conformance suite: handshake, tools/list, tools/call, error paths
Claude Code claude mcp add then claude mcp list reports ✓ Connected
Codex CLI Registration accepted (codex mcp add / get). Codex only opens the connection inside a logged-in session, so a live handshake is not verified
Cursor, Continue, others Not verified. They speak the same protocol, and the variations below are covered, but no one has run them against this

Rather than assume clients behave like the reference one, the wire-level differences are pinned by test — because a client that is not the reference is exactly what will find them:

  • older negotiated protocol versions, back to 2024-10-07, tested end to end against the real subprocess;
  • JSON-RPC batches, which MCP carried until 2025-06-18 removed them. An object-only check drops an array silently, which to a batching client looks like a server that simply never answers;
  • id: 0 (falsy, easy to drop), string ids, and id: null (answered as an invalid request rather than addressed to nobody);
  • CRLF line endings;
  • a client that sends no params on initialize, and one that sends an unknown cursor on tools/list;
  • stdout kept cleanconsole.log/info/warn/debug/trace are routed to stderr before serving. Nothing on this path prints today, but that is a property of the whole import graph, and one console.log added three modules away would corrupt the frame stream for every client with no test failing.