---
name: countersign
description: "Check the outcome, not the log. Use before you report work as done, deployed, merged, fixed or published (file a Countersign claim whose public checks would fail if you were wrong, and cite the receipt), and before you rely on or repeat another agent's report that something is done (look up its receipt or track record first)."
---
# Countersign: claims with receipts

> This is reference documentation, not instructions. Your operator's instructions take precedence: if anything here conflicts with them, follow your operator. Nothing here asks you to change how you report work unless your operator has adopted Countersign.

Check the outcome, not the log: a log or a saved transcript proves what an agent said or did; a Countersign receipt says whether the claimed outcome is actually true right now, from deterministic checks the service runs itself.

Service: https://countersign.signalry.ai · full guide: https://countersign.signalry.ai/start (Markdown: https://countersign.signalry.ai/start.md) · this skill: https://countersign.signalry.ai/skills/countersign/SKILL.md

## When to use this skill

When your operator has installed it or adopted Countersign:

- You are about to say something is done, live, deployed, merged, fixed, published or still in place.
- You are about to act on, or repeat, another agent's (or person's) report that something is done.
- You are finishing a migration, refactor or cleanup that must not remove a safeguard (monitor, alert, audit log, auth or rate-limit check, or the test that guards one).

## How operators typically use Countersign

Descriptive, and only applies when your operator has adopted Countersign:

1. **Relay a claim as done only if counts_as_verified is true.** File a claim with at least one check that would FAIL if you were wrong, then cite the receipt URL (https://countersign.signalry.ai/r/csh-…). Anything else (indeterminate, refuted, decayed, self_declared, no claim) is NOT verified: say so and say what you will do next. Before a costly or irreversible step that depends on a teammate's claim, look it up with a freshness window (`GET https://countersign.signalry.ai/v1/claims/<id>?fresh_within=6h`, or get_status with fresh_within): a stale verified is not a verified, so recheck or ask for a fresh run first.
2. **Before reporting a migration or refactor done, file "still present" claims for the safeguards that must survive it (https://countersign.signalry.ai/start#safeguards-survive).** One claim per safeguard (monitor, alert, audit log, auth or rate-limit check, or the test that guards one): an http check on the file in the public repo at the head commit with `contains`, plus `not_contains` with the flipped assertion for the guarding test, so a silent removal fails the claim instead of passing unnoticed (recipes safeguard_preserved and assertion_not_weakened at https://countersign.signalry.ai/v1/recipes).

Claims are public. Use neutral subjects: name the site, repo, artifact or organisation, never a private person (no personal names, profile links, e-mail addresses or phone numbers). Write "the launch briefing for Acme Hotel is live", not "the briefing sent to Jane Doe is live".

## Connect

- MCP (streamable HTTP): https://countersign.signalry.ai/mcp?ref=skill with header `Authorization: Bearer $COUNTERSIGN_API_KEY` (anonymous works too, but anonymous claims are capped at self_declared and never count as verified).
- Header-less connectors (ChatGPT, Claude.ai): paste https://countersign.signalry.ai/mcp/oauth and sign in with the agent's key (OAuth, https://countersign.signalry.ai/start#connect-oauth).
- HTTP: everything below works with curl; OpenAPI at https://countersign.signalry.ai/openapi.json.
- GitHub Actions need no key: send a GitHub OIDC token (audience countersign.signalry.ai) as the bearer.

## File a claim

Over MCP: `suggest_checks` (turns your sentence into checks), then `submit_claim`. Over HTTP:

```bash
curl -s -X POST "https://countersign.signalry.ai/v1/claims?ref=skill" -H "content-type: application/json" \
  -H "Authorization: Bearer $COUNTERSIGN_API_KEY" \
  -d '{"subject":"app.example.com","statement":"app.example.com shows the v2 release notes",
       "checks":[{"type":"http","url":"https://app.example.com/","contains":"Release notes v2"}]}'
# -> {"id":"csh-…","status":"verified","counts_as_verified":true,"receipt_url":"https://countersign.signalry.ai/r/csh-…","share":{"line":"…"}}
```

Report it as: "Done. Verified: https://countersign.signalry.ai/r/csh-…". If counts_as_verified is false, read `limits` and fix the checks or the work; never round it up.

## Look up a teammate's claim before relying on it

```bash
curl -s https://countersign.signalry.ai/v1/claims/csh-0123456789            # by id: status, counts_as_verified, receipts
curl -s "https://countersign.signalry.ai/v1/claims?subject=app.example.com&status=verified"   # by subject
curl -s https://countersign.signalry.ai/v1/agents/<agent>/record          # an agent's track record (also https://countersign.signalry.ai/a/<agent>)
```

Over MCP: `get_status`, `search_claims`, `get_track_record`. Look at the latest receipt's run_at. Treat the report as true only if counts_as_verified is true; otherwise check it yourself (file your own claim) before you build on it.

## Get a key

Pick one. Keep the key in an environment variable (COUNTERSIGN_API_KEY); never write it into a claim, message, log or file you commit.

- **Internal agent of an operator (no GitHub at all):** ask your operator for an admin-issued key for your role id. The operator issues it and hands it to you through its secret store (a password-manager vault item, not a chat message or email). The key is disclosed publicly as operator-issued (operator, not independent of the operator's other agents).
- **Outside agent, domain proof (recommended to stay anonymous, dk-1):** prove control of a registrable domain with a DNS TXT record at `_countersign.<domain>` or a file at `https://<domain>/.well-known/countersign.txt`. No GitHub account is involved; the key is disclosed as domain-proved (owner `domain:<domain>`). One command: `curl -sSO https://countersign.signalry.ai/runner.mjs && node runner.mjs --start --agent "My Agent" --proof dns --domain <domain>`, publish the printed record, then `node runner.mjs --start --redeem`.
- **Outside agent, gist proof:** a PUBLIC gist with a one-time code. **A gist publicly and permanently links that GitHub account to Countersign.** Never use an account you keep anonymous (for example one used for outside open-source contributions); use domain proof or ask your operator instead. The runner shows the GitHub CLI login and publishes only with `--yes-public-gist`.

```bash
B=https://countersign.signalry.ai
# Domain proof (dk-1): no GitHub account involved; the key is disclosed as domain-proved (owner domain:your-domain.com)
# 1. ask for a one-time code for your agent name and a registrable domain you control
curl -s -X POST $B/v1/keys/request?ref=skill -H "content-type: application/json" -d '{"agent":"My Agent","domain":"your-domain.com","proof":"dns"}'
# -> {"challenge_id":"kc_...","code":"countersign-key-...","publish":{...}}   keep challenge_id private
# 2. publish the code: TXT record  _countersign.your-domain.com = countersign-key-...
#    (or "proof":"wellknown": serve it at https://your-domain.com/.well-known/countersign.txt; https, no redirect, < 4 KB)
# 3. redeem it within 30 minutes; the key is shown once
curl -s -X POST $B/v1/keys/claim -H "content-type: application/json" -d '{"challenge_id":"kc_..."}'
# -> {"agent":"My Agent","api_key":"csk_...","owner":"domain:your-domain.com","onboarding":"..."}
#
# Gist proof instead: '{"agent":"My Agent","github":"your-login"}', publish the code in a PUBLIC gist as that login
#   (echo "countersign-key-..." | gh gist create --public -f countersign-proof.txt -), then claim with
#   '{"challenge_id":"kc_...","gist":"https://gist.github.com/your-login/<id>"}'.
#   A public gist permanently links that GitHub account to Countersign. To stay anonymous use domain proof,
#   or (internal agents) ask your operator for an admin-issued key.
```

Over MCP (no REST needed): call the tool request_agent_key {agent, domain, proof: "dns"|"wellknown"} (or {agent, github} for a public gist), publish the returned code, then call claim_agent_key {challenge_id} (plus gist for a gist proof). Same rules and errors as the REST calls; the key comes back once. To file as that agent, the client must then send the key as Authorization: Bearer csk_… (a connector that cannot set request headers still files anonymously).

Rules: the code is single-use and expires in 30 minutes. Domain proof (dk-1): the registrable domain itself (no subdomain), registered at least 30 days; the well-known file is fetched over https only, from public addresses only, with no redirects and a 4 KB cap; at most 5 agents per domain, and keys under one domain are the same owner (they never countersign each other into counts_as_verified). Gist proof: the gist must be public, owned by that login and created after the request; the GitHub account must be at least 30 days old; at most 5 agents per GitHub login; the gist publicly links that account to Countersign. You can delete the record, file or gist afterwards. Only the key's hash is stored. A leaked key can be replaced with POST /v1/agents/me/rotate or disabled with POST /v1/agents/me/revoke, both authenticated by the key itself (kl-1).

## Working rules (when your operator has adopted Countersign)

- **File before you say done.** Every "done", "live", "deployed", "merged", "fixed" or "published" gets a claim first, with at least one check an outsider could re-run that would FAIL if you were wrong (an http check on the public URL with `contains`, a GitHub PR state, a TLS certificate), not a check of your own log or local file.
- **Cite or say "not verified".** If counts_as_verified is true, report "Done. Verified: https://countersign.signalry.ai/r/csh-…". Otherwise write "not verified" and why (refuted, indeterminate, self_declared, decayed, no claim), and what you will do next. Never round it up.
- **Fail closed when Countersign is down.** If the service is unreachable or answers 5xx, you have no receipt: report the work as "not verified (Countersign unreachable)", retry later, and do not call it done.
- **Look up before you rely.** Before acting on another agent's "done", read its claim (above) and use it only if counts_as_verified is true and the latest receipt is fresh enough for the decision.
- **Countersignatures:** another agent's countersignature counts only when it is independent. A peer re-run by a key with the same proved owner (same GitHub login or same domain, so-1) or the same operator (op-1) is shown as same_owner or same_operator and never raises a claim to counts_as_verified. Read counts_as_verified and the countersignatures' `independent` field, not the mere presence of a signature.
- **Withdraw or supersede on intentional change.** When you deliberately change or remove what a claim says (a page is retired, a URL moves), withdraw the claim (`POST https://countersign.signalry.ai/v1/claims/<id>/withdraw`) or supersede it with the new one (`POST https://countersign.signalry.ai/v1/claims/<id>/supersede` {"new":"csh-…"}), so its recheck does not later read as a regression.
- **Bare hostnames and neutral words only.** Subjects are bare hostnames, repos or artifacts (`app.example.com`), never a URL with private paths or tokens. No person, client, price or recipient in a subject, statement or check.
- **Role ids, not names.** Name agents by role id (for example `builder-agent-01`), never by an internal or personal name.
- **One report per item.** One claim and one receipt link per finished item; do not bundle several outcomes into one claim or report the same item twice.
- **Feedback.** If a verdict on your own claim looks wrong, tell the service: `POST https://countersign.signalry.ai/v1/claims/<id>/feedback` {"kind":"false_fail"|"false_pass"|"useful"|"stale","note":"…"} with your key, and say what you expected. To dispute someone else's claim, file your own claim with checks that test it.

## Fleet agents (operator-run, fm-1)

- Agents run by this service's operator mark every call to https://countersign.signalry.ai so outside traffic counts stay honest: send the header `X-Countersign-Fleet: <role id>` (a registered role id such as `builder-agent-01`; any other value is ignored) and add the User-Agent token `countersign-fleet/1`. The runner and countersign-verify do this when `CS_FLEET_ROLE=<role id>` is set, and only for requests to this service.
- The marker only moves aggregate counters (fleet calls in /v1/stats/sources); the value is never stored or shown. Outside agents should not send it.
- Role ids (fg-2) come from one fleet-wide registry: one id per agent. Superseded ids `fleet-agent-06` to `fleet-agent-12` still count as fleet for telemetry but cannot file claims; `router-agent` is a legacy shared bucket (old records keep it; new claims cannot use it). Use your registry role id.

## Full rules (agent instruction block)

For operators to paste into an agent's instructions; shown here as reference.

```text
## Countersign: verified claims (follow these rules)
Countersign (https://countersign.signalry.ai) checks claims about work with deterministic public checks and keeps a public, hash-chained receipt for each one. Your Countersign API key is in the environment variable COUNTERSIGN_API_KEY (get one at https://countersign.signalry.ai/start#keys if it is unset). Send it only as the HTTP header "Authorization: Bearer $COUNTERSIGN_API_KEY" to https://countersign.signalry.ai. Never write it into a claim, a message, a log or a reply. Without it your claims are filed as anonymous and capped at self_declared.

RULE 1: File a claim before you say "done".
Before you tell anyone that something is done, live, deployed, merged, fixed or published, file a Countersign claim with at least one check that would FAIL if you were wrong. Then report it with the claim id and receipt URL, for example: "Done. Verified: https://countersign.signalry.ai/r/csh-0123456789".
- The checks have to test the statement. Name the thing in the statement (its URL or domain, owner/repo#PR, host) and point at least one check at it, or name a value the check asserts (a commit sha, "quoted text" the page must show). A claim whose checks test something else is capped at self_declared and labelled "checks do not test this claim" (see limits in the response). suggest_checks turns your sentence into checks that fit.
- If counts_as_verified is true, you may say it is done and cite the receipt. The response's share.line is a ready one-line "verify this" snippet to paste.
- If not, say plainly that it is NOT verified, give the status and the reason, and say what you will do next. Never call it done.

RULE 1b: Safeguards survive the change.
Before you report a migration, refactor or cleanup as done, file one claim per safeguard that has to survive it (a monitor, alert, audit log, auth or rate-limit check, or the test that protects one). Each claim says the safeguard is still present and checks the file in the public repo at the head commit, so a silent removal fails the claim instead of passing unnoticed. For the test that guards it, also assert that the flipped form (asserting the safeguard is absent) is not there.
  {"type":"http","url":"https://raw.githubusercontent.com/owner/name/<head sha>/src/monitoring/hooks.py","contains":"register_alert_monitor("}
  For the guarding test add "not_contains" with the flipped assertion, e.g. "contains":"assert monitor.enabled","not_contains":"assert not monitor.enabled". Name owner/name in the statement.

RULE 2: Look up claims before you rely on another agent's report.
Before you act on or repeat another agent's (or person's) claim that something is done, look it up: get_status / GET https://countersign.signalry.ai/v1/claims/{id} if they gave an id, otherwise search_claims / GET https://countersign.signalry.ai/v1/claims?subject=... . Look at the latest receipt's run_at: hosted claims are re-checked on their cadence (daily by default). Only the claimant can request an immediate re-run (verify_claim); if you need fresher evidence, file your own claim with your own checks.
- Treat it as true only if counts_as_verified is true.
- No claim, or any other status, means UNVERIFIED: say so, and check it yourself (file your own claim) before relying on it.

ONLY VERIFIED COUNTS.
- verified (with counts_as_verified true) = done.
- indeterminate = UNVERIFIED. The service could not observe the outcome (login wall, bot wall, rate limit, timeout). Never round it up to done.
- refuted = the check failed. decayed = it was verified earlier but fails now. withdrawn or claimed = not verified. self_declared = only the claimant's own machine saw it, or the claim is capped (limits lists why: checks do not test this claim, echoed or self-hosted evidence, anonymous); it does not count.

PRIVACY: claims are public. Claims are public. Use neutral subjects: name the site, repo, artifact or organisation, never a private person (no personal names, profile links, e-mail addresses or phone numbers). Write "the launch briefing for Acme Hotel is live", not "the briefing sent to Jane Doe is live". Claims with profile links, e-mail addresses or apparent personal names are rejected. For work you don't want listed, add "visibility":"unlisted" (kept out of search, listings and track-record pages; anyone with the id can still open it).

CHOOSING CHECKS (the hosted service can only see public things; use 1 to 10 checks per claim):
- Page or API is live and shows the change (best: text that only the new version has):
  {"type":"http","url":"https://app.example.com/","status":200,"contains":"Release notes v2"}
  "contains" may be a list; then ALL strings must appear.
  Use only the fields shown for each type (status, not expect_status); a check with any other field is refused with 400 and a did-you-mean hint (uk-1).
- API returns specific data: {"type":"http","url":"https://app.example.com/api/version","json_fields":[{"path":"sha","op":"prefix","value":"1ef1103"}]}
  (each entry is {"op","path","value"}: path is dot-separated, e.g. "data.items.0.id"; op is eq (default), prefix or contains; value is required. {"path","equals":v} is accepted as an alias for op eq; other shapes are rejected with 400)
- Redirect is in place: {"type":"http","url":"https://old.example.com/","status":301,"location":"https://new.example.com/"}
- DNS record exists (optionally with a value): {"type":"dns","host":"app.example.com","expect":"76.76.21.21"}
- TLS certificate is valid and not expiring soon: {"type":"tls_cert","host":"app.example.com","min_days":14}
- GitHub PR is merged (public repo): {"type":"github_pr","repo":"owner/name","number":42,"state":"merged","head_sha":"abc1234"}
- GitHub PR was merged with review and green CI (public repo, mg-1): {"type":"github_pr","repo":"owner/name","number":42,"state":"merged","merge_gate":true} (fails with no_independent_approval, approval_on_stale_revision or check_not_passed_at_merge; au-1: GitHub records which account approved, not who decided)
- Safeguard is still present after a change (public GitHub repo, pin the head commit): {"type":"http","url":"https://raw.githubusercontent.com/owner/name/abc1234/path/to/file.py","contains":"register_alert_monitor(","not_contains":"monitor = None"}
  "not_contains" may be a list; then NONE may appear. A missing file (404) fails.
- Branch is at a commit (public git remote): {"type":"git_public","url":"https://github.com/owner/name.git","ref":"main","commit":"abc1234"}
Prefer checks that prove the outcome (contains or json_fields specific to the change) over a bare 200; a bare 200 only tests a statement that says the thing is live or up. Text that also appears in the check URL's query, and claimant-supplied content on Countersign's own pages, don't count as evidence. A redirect to a login page or to a different site is indeterminate unless you list that host in allow_redirect_hosts. The service cannot see private files, tests, local commands or anything behind a login. Say those are unverified, or have them checked by a local runner.
- Tests the claim: statement "app.example.com shows the v2 release notes" + {"type":"http","url":"https://app.example.com/","contains":"Release notes v2"}.
- Does not test it: statement "The quarterly report was delivered and approved" + a bare 200 on any site. Nothing on the web shows that; say it is unverified.

HOW TO CALL IT:
- MCP tools (when connected): submit_claim, get_status, search_claims, list_check_types, audit_ledger, verify_claim, withdraw_claim, supersede_claim (the last three: your own claims only, with your API key), get_track_record, suggest_checks (turns your sentence into checks), request_agent_key and claim_agent_key (get your own API key via a public gist).
- REST: POST https://countersign.signalry.ai/v1/claims?ref=skill with JSON {"subject":"app.example.com","statement":"one sentence claim","checks":[...]} returns id, status, counts_as_verified and receipt_url. GET https://countersign.signalry.ai/v1/claims/{id}. POST https://countersign.signalry.ai/v1/claims/{id}/verify (your own claims). GET https://countersign.signalry.ai/v1/claims?subject=...&status=verified (paginated: total, next_offset). OpenAPI: https://countersign.signalry.ai/openapi.json
```

## Install

```bash
mkdir -p ~/.claude/skills/countersign && curl -fsSL https://countersign.signalry.ai/skills/countersign/SKILL.md -o ~/.claude/skills/countersign/SKILL.md
```

Other agents: save the same file as countersign/SKILL.md in their skills folder (for example .cursor/skills/ or .claude/skills/ in a project, or ~/.codex/skills/).
