The Tersign plugin for Claude Code, published by Tersign — the record
layer for the machine age. It bundles the MCP server, the tersign-evidence skill, and a
/verify command.
Venues rotate. The transcript endures.
/plugin marketplace add tersignhq/skills
/plugin install tersign@tersign
No configuration to verify or to record disclosures. No API key, no signup. The first
record_disclosure call self-provisions a signer-keyed account with a key generated on your
machine (see What this plugin runs, sends and stores).
Every setting is optional. Leave them all empty and the server behaves exactly as tersign 0.6.7
does when started with no configuration.
| Setting | What it is | Needed for |
|---|---|---|
seller_key (sensitive) |
A 0x-prefixed 32-byte hex private key to sign with, instead of the generated one | Bringing a key you already use |
ledger_url |
Base URL of the ledger, normally https://tersign.ai |
Counter-signed receipts, refunds, the dispute tools |
ledger_api_key (sensitive) |
API key of a Tersign seller account | Counter-signed receipts, refunds, respondent evidence |
ledger_seller_id |
Seller ID of that account | Counter-signed receipts, refunds |
Set them inside Claude Code with /plugin configure tersign@tersign, the plugin's configuration
dialog, which masks the sensitive values. The /config panel also lists ledger_url and
ledger_seller_id. From a shell, claude plugin configure tersign@tersign shows which settings
are set. Claude Code keeps the sensitive values in your operating system's credential store and
the others in your user settings.json.
The plugin passes the settings to the server as TERSIGN_SELLER_KEY, TERSIGN_LEDGER_URL,
TERSIGN_LEDGER_API_KEY and TERSIGN_LEDGER_SELLER_ID, empty when unset. Variables of those
names exported in your shell do not reach the server; use the settings instead. When a tool's
error names one of those variables, set the matching setting.
Three more variables reach the server from the environment Claude Code runs in, when they are set
there: TERSIGN_ISSUER_NAME, TERSIGN_ISSUER_JURISDICTION and TERSIGN_ISSUER_TAX_ID. They are
not credentials and not plugin settings. issue_receipt puts them in the compliance-fields record it
signs and sends to the ledger, as the table under
What this plugin runs, sends and stores shows.
npx -y tersign@0.6.7 verify 0xe5874f1ffe87f0a6dd9eb157730f67b86ee4538b125fe30fcc4e165213dd3fc4That is the genesis receipt, seq 1, on the production chain. A digest is a lookup: the public
ledger reports the record, its chain position and the anchored commitment covering it, and the CLI
labels the answer VALID (ledger-reported) because nothing is recomputed on your machine. No
account, no API key, and nothing about it depends on this plugin. A receipt file's signature and digest
are checked on your machine; a bundle's chain and commitment are recomputed offline, as below.
MCP server (npx -y tersign@0.6.7, stdio) |
Nine tools: issue_receipt · verify_receipt · record_disclosure · verify_compliance_record · record_refund · open_dispute · submit_dispute_evidence · adjudicate_dispute · get_dispute |
Skill tersign-evidence |
Triggers on evidence, chain of custody, receipt verification, dispute filing, audit trail, disclosure records. Carries the recompute recipes so an agent can check a chain by hand rather than believing a status field. |
Command /verify |
Verifies a receipt file or a whole evidence bundle on your machine, or looks a digest up on the ledger — and reports the verdict without strengthening it. |
Verification needs nobody's permission. Every check above runs against a counterparty's
artifact with no account. Checking a receipt file touches no chain and no ledger; it runs through
npx, which may fetch the tersign package from the npm registry when it is not cached. The
evidence-bundle checker is stdlib Python and, once fetched, runs with no network at all. Using Tersign's services, including the record_disclosure tool, is subject to the
Terms, which include business-only use and the data-use terms in
Section 9 that apply to free-tier content.
A PASS is not automatically authorship. The bundle checker prints one of three passing verdicts:
PASS (integrity-only), without--signer: the bundle is internally consistent and tamper-evident, but every signer identity was read from the bundle itself. It also stays integrity-only when the ledger key is a published test key, as in the synthetic example archives, or when no party signature recovers to--party; the verdict line says why.PASS — integrity AND counter-signer authorship, when every counter-signature and the anchor signature recover to the--signeraddress, which you take out of band from https://tersign.ai/v1/ledger. That binds the ledger's counter-signatures; it does not establish authorship of the records.PASS — integrity AND authorship, only when, in addition, every party signature recovers to the--partyaddress, which you obtain out of band from the party itself.
The checker also prints an UNAUTHENTICATED line naming the fields nothing signs. A verdict should
never be stronger than its checks, including ours.
For an evidence bundle handed to you by an interested party, fetch the checker out-of-band rather
than running the copy inside the archive — a bundle can ship a checker that blesses it. Fetch the
release the archive was built with, which section 0 of its VERIFY.md names on its base= line
(https://tersign.ai/verify/sha256/<digest>, or https://tersign.ai/verify/v1 in earlier
archives). Put the bundle's directory between the single quotes on the first line, writing any
single quote in the path as '\'' (inside single quotes, nothing else in the path is expanded):
bundle='<bundle-dir>'
[ "$(grep -c '^base=' "$bundle/VERIFY.md")" = 1 ] &&
base=$(sed -n 's/^base=//p' "$bundle/VERIFY.md") &&
printf '%s\n' "$base" | grep -Eqx 'https:/{2}tersign\.ai/verify/(v1|sha256/[0-9a-f]{64})/?' &&
base=${base%/} && OOB=$(mktemp -d) &&
curl -fsSL "$base/SHA256SUMS" -o "$OOB/SHA256SUMS" &&
case $base in */v1) pin=244af66f22afe06d36adce9880b9aa3b003306e26789cfb08d8051ef6c816993 ;; *) pin=${base##*/} ;; esac &&
( cd "$OOB" && echo "$pin SHA256SUMS" | shasum -a 256 -c ) &&
curl -fsSL "$base/verify_bundle.py" -o "$OOB/verify_bundle.py" &&
curl -fsSL "$base/keccak.py" -o "$OOB/keccak.py" &&
curl -fsSL "$base/secp256k1.py" -o "$OOB/secp256k1.py" &&
( cd "$OOB" && shasum -a 256 -c SHA256SUMS ) &&
python3 "$OOB/verify_bundle.py" "$bundle" &&
diff -r -x SHA256SUMS "$OOB" "$bundle/verify"Every fetch lands in a new temporary directory, never in the bundle, and from the second line on,
each line runs only if the one before it succeeded. The grep -c, sed and grep -Eqx lines
use the address only if it is exactly https://tersign.ai/verify/v1 or
https://tersign.ai/verify/sha256/<digest> with <digest> 64 lowercase hex digits (a trailing
slash is optional); if VERIFY.md names anything else, or nothing, stop rather than choosing a
release for it. The case line picks the digest the fetched SHA256SUMS must hash to, and the
line after it checks that before anything fetched runs: the digest in a content address, or, for
https://tersign.ai/verify/v1, 244af66f…6993, the content address of the 2026-10-07 release
that address serves. For any stop before the python3 line (the address lines, a curl failure
or 404, a digest FAILED, a SHA256SUMS mismatch), report that step's output and stop;
never fetch another release instead. If the only failure is files.closedSet and every extra
file it names is under verify/__pycache__/ or oob/ (an earlier archive's own steps can leave either there), re-run on a fresh copy of the archive (a fresh
unzip into a new directory made with mktemp -d) and report both runs.
No published release can read an archive whose manifest.json names "seller", not "party"
(the manifest format used before the published checker releases). If the python3 line prints
FAIL bundle.unreadable, no published Tersign release can check this archive out of band, and the
archive cannot be verified that way: say so plainly, and report every FAIL line the published
checker printed before it, which still count. Never run the checker inside the archive instead
(it is the copy a bundle can ship to bless itself), and never call the archive older than it
claims to be.
The last line diffs the fetched files against the bundle's verify/. A difference is itself the finding unless the archive is older than the checker at
that address: an archive built before 2026-10-08 can carry an earlier checker than
https://tersign.ai/verify/v1/ serves. The newest release is at https://tersign.ai/verify/latest/
and every release is listed at https://tersign.ai/verify/releases.json; a release other than the
one an archive names differs from its verify/ by design.
/verify does this for you.
What it runs. Claude Code starts the MCP server with npx -y tersign@0.6.7, over stdio. npx
downloads the tersign package, version 0.6.7, and its
dependencies (@modelcontextprotocol/sdk, viem, zod and theirs) from the npm registry your npm
configuration names (https://registry.npmjs.org by default), then runs it on your machine. The pin
covers tersign itself: the package declares its dependencies as version ranges (^1.29.0,
^2.21.0, ^3.23.0) and ships no lockfile, so npx resolves them from the registry when it
installs, and the versions you get are the ones those ranges resolve to at that time. Its source
is at https://github.com/tersignhq/tersign-js. On macOS, when no seller_key is set, the server
runs security, the system keychain tool (looked up on your PATH), to read its signing key from
the keychain and, when it has to generate a key, to find your default keychain and store the key
there. The plugin has no hooks, and it and the server run nothing else on their own.
What the MCP tools send. {ledger} is ledger_url when set. When it is empty,
record_disclosure uses https://tersign.ai, and the tools that need a ledger URL return a
configuration error without any network request.
| Tool | Network request | What it sends |
|---|---|---|
verify_receipt, verify_compliance_record |
None | Nothing: they run offline |
record_disclosure |
POST {ledger}/v1/disclose |
A disclosure record signed with your key: the agent ID, the disclosure kind, the medium and resource URL when given, the time, and a digest of the disclosure text. The text itself never leaves your machine. No API key is sent; the first call creates a free account bound to your key's address |
issue_receipt |
POST {ledger}/v1/receipts, only when ledger_url, ledger_api_key and ledger_seller_id are all set; otherwise none |
The API key as a bearer token, the seller ID, the signed receipt (network, resource URL, payer address, time, transaction hash) and its compliance-fields record (supply description, tax scheme and currency, principal when given, the issuer name and jurisdiction, which are unnamed seller and unknown unless TERSIGN_ISSUER_NAME or TERSIGN_ISSUER_JURISDICTION is set in the environment Claude Code runs in, and an issuer tax ID only when TERSIGN_ISSUER_TAX_ID is set there) |
record_refund |
POST {ledger}/v1/refunds, with the same three settings |
The API key as a bearer token, the original receipt digest, the amount and the reason |
open_dispute |
POST {ledger}/v1/disputes, when ledger_url is set |
A dispute signed with your key: the receipt digest, the reason, the claimed amount, the statement when given, and the time |
submit_dispute_evidence |
POST {ledger}/v1/disputes/{digest}/evidence, when ledger_url is set |
Evidence signed with your key: the dispute digest, your role, and each artifact's kind, digest, time and note (digests only; the content stays with you). As respondent, also the API key as a bearer token when one is set |
adjudicate_dispute |
POST {ledger}/v1/disputes/{digest}/adjudicate, when ledger_url is set |
The dispute digest, in the address |
get_dispute |
GET {ledger}/v1/disputes/{digest}, when ledger_url is set |
The dispute digest, in the address |
The server makes no other network requests. Your signing key is never sent: only signatures made with it are.
What is permanent, and what can be erased. The ledger is append-only and hash-chained, and its chain commitments are anchored publicly. For a record it counter-signs into a chain (a disclosure or a receipt), the digest, the counter-signature, the chain links and the anchors are permanent, and any alteration of the record is detectable: by anyone holding the counter-signed record, and, once an anchor covering it is confirmed, by anyone holding its digest. Stored record content is kept for seven years by default, deleted when its retention period expires, and erasable on request, as Section 11 of the Terms sets out. Under Section 11.4, erasure through the API keeps a payment receipt's payer address and transaction hash where each has the form set out in Section 11.5, for the establishment, exercise or defence of legal claims (the payer's dispute rights); one not kept is treated as absent. For an account that has accepted only a version of the Terms earlier than v1.4, erasure deletes the payer address and transaction hash as well, as that version provides: the payer can then no longer open a dispute on that record, the duplicate-charge check no longer finds it, and a dispute still open on it or on another record is decided without them. A dispute record already opened on the record is kept unchanged, including the claimant's address, the signed dispute, and any adjudication inputs and rationale, which may include the transaction hash; so are the record's refund records, including the refund amount and reason. Anyone who already holds a copy of a record keeps it, and anyone holding a record's digest can look it up. Put no personal data into an agent ID, a statement, a note, a supply description or any other field you send.
What the command and the skill run. /verify tells Claude to run:
npx -y tersign@0.6.7 verify <0xdigest>, which asks the ledger (https://tersign.ai, or the one named with--ledger) forGET /v1/receipts/<digest>/verify, andnpx -y tersign@0.6.7 verify <receipt.json> --signer 0x…, which checks a receipt file on your machine and contacts no chain and no ledger. npx fetchestersignand its dependencies from the npm registry when they are not already cached.- For an evidence bundle, the
curllines above: they downloadSHA256SUMS,verify_bundle.py,keccak.pyandsecp256k1.pyfrom the verifier release the bundle'sVERIFY.mdnames (https://tersign.ai/verify/sha256/<digest>/) into a new temporary directory, checkSHA256SUMSagainst the release's digest and the files againstSHA256SUMSwithshasum, runpython3 verify_bundle.pyon your machine against the bundle (standard library only, no network), anddiffthe release against the bundle's own copy. When the only failure isfiles.closedSetoververify/__pycache__/oroob/,unzipof the archive into a new temporary directory, and the same lines on that copy. - For a standalone action record,
npm install --prefix <temporary directory> --save-exact --ignore-scripts tersign@0.6.7into a new temporary directory, which downloadstersignand its dependencies from the npm registry without running their install scripts, thennode --input-type=module -e …in that directory, which callsverifyActionRecordon the record file on your machine and contacts no chain and no ledger. curltohttps://tersign.ai/v1/ledger(GET), for the ledger's counter-signing address, which the checker takes as--signer.- When you ask for more,
curltohttps://tersign.ai/v1/receipts/<digest>/verifyorhttps://tersign.ai/v1/receipts/<digest>/envelope.
The tersign-evidence skill runs nothing by itself. It instructs Claude to recompute chains rather
than trust a status field, to use the same public endpoints (/v1/receipts/<digest>/verify,
/v1/receipts/<digest>/envelope?venue=…, /v1/ledger) and npx -y tersign@0.6.7 verify, to
fetch a bundle's checker from the address in its VERIFY.md, and to use the MCP tools above. For a
seller wiring receipts into their own server it shows npm install --save-exact tersign@0.6.7;
when that is run, npm writes node_modules/, package.json and package-lock.json into the
project directory it runs in.
What it stores. When no seller_key is set, the server reads its signing key from the macOS
keychain (service tersign-signer), else from ~/.tersign/signer.key. If neither exists, it
generates a key on first start and stores it in the keychain, or, where no keychain is available,
in ~/.tersign/signer.key (mode 0600, in a 0700 directory). That key is your evidence identity:
back it up. Claude Code stores the plugin settings as described under Settings, and
npm keeps the downloaded package in its cache (~/.npm by default). Nothing else is written,
apart from the new temporary directories /verify creates (one for a bundle's checker, one for
the fresh unzip of an archive when it re-runs the checker on a fresh copy, and one for the
tersign package when it checks a standalone action record, where npm writes node_modules/,
package.json and package-lock.json), the bytecode cache some Python installations keep for
their own standard library (the macOS /usr/bin/python3 keeps one under
~/Library/Caches/com.apple.python) and, when the skill's npm install --save-exact tersign@0.6.7
is run, the files npm writes into that project.
.claude-plugin/plugin.json the plugin manifest, with the optional settings
.claude-plugin/marketplace.json so this repo is installable as a marketplace
.mcp.json npx -y tersign@0.6.7, over stdio, settings passed as its environment
skills/tersign-evidence/ the skill
commands/verify.md /verify
tersign.ai · tersign on npm ·
tersign on PyPI (offline verifier, zero dependencies) ·
TypeScript SDK + MCP server ·
conformance vectors ·
MCP registry as
io.github.tersignhq/evidence
Apache-2.0 (earlier versions were MIT). Acceptance and evidentiary weight are always the receiving venue's decision — an export mapping is never a compliance certification.