Skip to main content

said-who

A memory store for AI agents that refuses to save a claim about what a person decided unless it can point at a real message that person actually typed, and quote their words back.

It supports Claude Code and nothing else. Claude Code writes its transcripts to disk and stamps every turn with an origin the agent does not author, which is the only reason any of this can be checked. For any other harness the entry is stored as hearsay and labelled as hearsay. Everything runs on your machine: no network, no database, no daemon, no account.

Correction, 4 October 2026. An earlier public usage example and related test fixtures included details drawn from a private working context. The current example and fixtures use fictional data, and the privacy statement below now applies specifically to the measured corpus. Earlier repository history and the first package release remain public.

Why

An agent wrote a business target into a decision log that the person it was attributed to had never said. Four further agents read it, believed it, and copied it into three more files. It was not stale. It was hours old and invented. A human noticed and killed it.

No expiry rule catches that, because the entry was new. No review cadence catches it, because every reader downstream had a plausible looking source. The only thing that catches it is provenance the agent making the claim cannot write.

A message id is a string, and an agent can invent one. A timestamp is a string, and an agent can invent one. What an agent cannot easily invent is the literal text a person typed, in a transcript the harness wrote, in a turn the harness marked as human. So an entry claiming a person decided something needs three things at once: a locator, a literal quote, and a transcript on disk where both hold. If any of the three fails, the write is refused. Not flagged. Refused.

What it does not prove

It proves the cited message exists, that the harness recorded it as a human turn, and that it contains the quoted words.

It does not prove the entry is a fair reading of that message. An agent can quote six real words of yours and attach a conclusion you never reached. That gap is irreducible, and it closes only by quoting enough. This makes forgery expensive. It does not make misinterpretation impossible.

It also proves nothing at all outside Claude Code, and it says so on every entry it cannot check.

Install

pip install said-who

Use

Fictional example

said-who add outings --title "the picnic location" --src human \
  --cite "fictional-session#fictional-message" \
  --quote "meet beside the blue fountain on Saturday" <<'BODY'
The picnic location is the one in the cited message.
BODY

The body arrives on stdin, because that is how an agent will actually use it.

Without a citation, the same entry is refused:

REFUSED [NO_CITATION] --src human needs --cite.
  A claim that a person decided something needs a locator and a quote, and the quoted
  words have to be in a turn the harness recorded as human.

The six commands:

said-who add <topic> --title T --src CLASS [--cite LOC --quote Q] [--tier T] [--review DATE]
said-who list [<topic>] [--src CLASS] [--due]
said-who verify [<topic>]
said-who review
said-who verdict <topic> <id> --confirm|--revise|--kill --by WHO [--reason R]
said-who doctor

verify re-resolves every citation in the store and reports which no longer hold, which is how you find out that a store carried to another machine has no proof behind it any more.

Source classes

Every entry declares one.

class means extra requirement
measured something was run and can be run again none
derived reasoned from other entries none
external a third party system said it none
human a person decided it locator, quote, and a verified transcript
human-unverified attributed to a person, not citable none, and permanently flagged as hearsay

The two refusals

One, the citation check. --src human without a resolvable, verified citation is refused. The quote is compared literally, after whitespace is collapsed and case is ignored. It is deliberately not fuzzy, because a paraphrase is not somebody's own words.

Two, the laundering check. A body filed under measured, derived or external may not assert that a person approved, decided or confirmed something. Without this, refusal one buys nothing: the same fabrication is simply filed as derived and the store is no better off.

The laundering check is text matching, and it is a speed bump on the obvious path, not a guarantee. A determined agent beats it by writing around it. It also has a false positive class it cannot avoid, because it cannot tell a person from a product name, so a sentence like "Gradle decided to rebuild everything" is refused although nobody approved anything. The cost is a rephrase. No claim is made here that it is airtight, and the package's own test suite records both the cases it catches and the one it gets wrong.

Writing, and why it is append only

Concurrent agents are the normal case, not the edge case, and a memory file written by two of them at once loses entries silently.

Measured on this machine by tests/test_concurrency.py, which implements the defective writer as well as the real one. Twenty writers, each a separate process, started together, each adding one distinct entry to one topic file. The naive writer reads the file, spends 0.4 seconds building the new content, then writes the whole file back. The real writer appends under an exclusive flock.

writer entries surviving out of 20
naive read, modify, write 1
append under a lock 20

Three consecutive runs gave the same figures. Rerun it with pytest tests/test_concurrency.py.

Every accepted entry is also mirrored to an append only JSONL journal that is never rewritten, so the record survives anything that later happens to the readable file.

Expiry: a verdict, never a quiet deletion

Different claims go stale for different reasons, so one clock is wrong.

tier review after for
permanent never how a person works, house rules, prohibitions
doctrine 180 days engineering and design rules, the default
situation 60 days org state, ownership, what tool exists
number 30 days any figure
commitment its own date an open loop with a real deadline

When something is due it needs a verdict: confirm, revise or kill. A kill requires a reason and an author. Killed entries move to an archive file with their reason, the journal keeps the original permanently, and every rebuild refuses to put a killed entry back.

Killing a claim is a recorded decision with an author, exactly like making one.

Storage

Plain files in a directory you choose, set with --store or SAID_WHO_STORE, defaulting to ~/.said-who.

<store>/<topic>.md                   the readable log
<store>/journal/<topic>.ndjson       append only, never rewritten
<store>/archive/<topic>.retired.md   killed entries, with their reason

Human readable, greppable, and friendly to git. A memory that needs a server running is a memory that is silently empty when the server is down.

The lock is POSIX flock, so this runs on macOS and Linux. Windows is not supported and is not tested, and claiming it without testing it would be the same kind of unearned claim the tool exists to refuse.

Adding a harness

One function, fixed shape, documented in said_who/harness/__init__.py:

def resolve(locator: str, root: pathlib.Path | None = None) -> Turn

It must set human to True only when the harness itself recorded that turn as coming from a person. If the flag is written by the agent that also writes the memory entry, the adapter proves nothing.

No adapter ships for a harness that has not been run against that harness's real transcripts. One verified harness is worth more than six unverified ones.

Proof

The suite is the tool's own evidence. It is not enough on its own, so this section reports what happened when the tool was pointed at real transcripts it did not write.

Run against a real corpus, 24 September 2026

The measured corpus contained 893 Claude Code transcript files, 95,131 records and 578 MB. Only aggregate counts, rates and failure classes are recorded here. No source record, message text, quote, locator, private business fact or private content from the measured corpus left the machine.

what was tried n result
transcript lines the reader could not parse 95,131 read 0
human turns found 1,313 resolved with text 1,311, resolved with empty text 2, failed 0
a genuine quote from a genuine human turn 400 sampled accepted 377, wrongly refused 0, no usable text 23
an invented quote against a real human turn 200 refused 200, wrongly accepted 0
a real turn the harness did not mark human 200 refused 200, wrongly accepted 0
an invented locator 200 refused 200, wrongly accepted 0
a real quote filed as derived with an approval claim in the body 93 refused 93, wrongly accepted 0

Resolving all 1,313 human turns took 9.1 seconds.

What the real corpus found that the tests did not

Short turns are ordinary. Of 1,313 real human turns, 4.4 per cent carried one word or none and 22.2 per cent carried two to five. The fixtures were all full sentences, so no test could have raised this.

It matters because a one word quote passes the citation check and proves very little. An agent guessing "yes", "go" or "approved" would land it. The check reported success and the success was worth less than it looked, which is the failure this tool exists to prevent, sitting inside the tool itself.

The fix is not a refusal, because refusing would throw away real decisions that were genuinely made in one word. A quote under three words is stored and the writer is told, in plain words, that the evidence is thin and to quote more where the turn allows it. WEAK_QUOTE_WORDS in refusals.py carries the threshold and the reason.

The rest of the evidence, all repeatable with pytest

  • 142 tests, including one per refusal and the five forgery attempts in tests/test_forgery.py
  • the concurrency figures above, with the defective writer implemented and actually run, not asserted
  • three guards were removed on purpose and the named tests watched to fail, then restored and watched to pass: the citation check, the laundering check, and the weak quote notice

What is still unproven

The corpus above is one person's transcripts from one machine. It is broad enough to have found a real defect and it is not a sample of how other people use Claude Code. Nobody outside this repository has run this tool against their own transcripts yet, and nothing here should be read as evidence that it survives contact with a setup that is not this one.

Built with an AI assistant

The author set the direction, boundaries and release standard. The implementation was written with AI assistance and reviewed before publication. Each commit trailer has a Co-Authored-By line that records the model used.

Licence

MIT. See LICENSE.

Elsewhere

The rest of these tools, and the writing about what real files did to them, is at waiga.github.io.

Metadata

Release files for said-who 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for said-who 0.1.1
File Size Uploaded
said_who-0.1.1.tar.gz 23.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for said-who 0.1.1
File Interpreter ABI Platform
said_who-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 53.7 kB

Release files / said_who-0.1.1.tar.gz

Download URL said_who-0.1.1.tar.gz
Size 23.9 kB
Tags Source
SHA-256 checksum
How to use checksums
543c872e58551583db7aa315d866bb9021a4309d10efd1902cae6e7550ba49bd
BLAKE2b-256 checksum
How to use checksums
672329b2334440f4d1d3551343ea3044fefdd66c29c974682fca6f72cd3373d2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release files / said_who-0.1.1-py3-none-any.whl

Download URL said_who-0.1.1-py3-none-any.whl
Size 29.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f83beae9361f4e272c129ce3243953f9e77a6897b212470ca96cce079e082106
BLAKE2b-256 checksum
How to use checksums
e4f6bc25d4f1a829dbb4f98f1207be911f18c6f8255770b833a34a061b2a78e6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release 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