skip to content

Compute the ToS hash

Hash the exact UTF-8 bytes of terms.text, avoid the common encoding mistakes, and update the hash when terms change.

awaiting reviewupdated view markdown ->
for humans

Goal: produce sha256:<64 lowercase hex> over the exact bytes of the complete terms text, and keep it in sync with what you serve.

The normative rule is ToS binding. The digest covers terms.text, not a URL, a summary, a rendered page or a JSON-encoded copy.

Node

Save as hash.mjs:

import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";

const text = readFileSync("terms.txt", "utf8");
const hash = "sha256:" + createHash("sha256").update(text, "utf8").digest("hex");
console.log(hash);

Run against a terms file that ends with a newline:

node hash.mjs

Recorded output on 2026-10-21:

sha256:6c18126f93de8d1972589ca94633ca82e76bd9ba1f0592618b0105753f3501d6

Shell

sha256sum terms.txt

Recorded output for the same file:

6c18126f93de8d1972589ca94633ca82e76bd9ba1f0592618b0105753f3501d6  terms.txt

Prefix the digest with sha256:. sha256sum hashes the file bytes, including a trailing newline. If you compute the value in a shell variable, use $(sha256sum terms.txt | cut -d' ' -f1); do not pipe through a command that strips trailing newlines from the text before hashing.

Test vector

fixtures/valid-full.json in agentregistrar/spec carries a complete terms text and its advertised hash. Recompute it from the JSON:

node --input-type=module -e '
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
const doc = JSON.parse(readFileSync("fixtures/valid-full.json", "utf8"));
console.log("sha256:" + createHash("sha256").update(doc.terms.text, "utf8").digest("hex"));
console.log(doc.terms.sha256);
'

Recorded output:

sha256:6357f96f625964c2eea9d04e24edfc5098bd9aae3623f5d96e0a441df0025967
sha256:6357f96f625964c2eea9d04e24edfc5098bd9aae3623f5d96e0a441df0025967

The validator recomputes the same digest and fails on a mismatch. It prints the expected and actual values, so a bad hash is a field-level error, not a mystery.

Do not hash the rendered page

The live VaporCloud discovery document advertises sha256:e854d9c2e7446cc132861235fa8e1c14c062c32c176b4f34d6845065cef5fec5 over its terms.text. Hashing the HTML page at the same URL never reproduces that value:

curl -sS https://skdl.dev/terms -o terms.html
sha256sum terms.html

The HTML digest is not reproducible, and that is the point. The page embeds a per-request Cloudflare challenge token (__CF$cv$params carries a fresh ray id), so three consecutive fetches on 2026-10-21 returned db012b90..., 6ca3d454... and 9b127a18...; an earlier recorded fetch gave 52f691a4.... Four digests for the same URL, none of them the advertised sha256:e854d9c2.... A digest you cannot reproduce is not a terms binding.

For a deterministic comparison, hash a local copy of the terms text and then hash a JSON-encoded wrapper of the same text:

node --input-type=module -e '
import { createHash } from "node:crypto";
const text = "Example RP Terms of Service v2026.1";
const hash = (value) => "sha256:" + createHash("sha256").update(value, "utf8").digest("hex");
console.log(hash(text));
console.log(hash(JSON.stringify({ text })));
'

Recorded output:

sha256:fbbb44ec069bfb7d5eb1da2a1c384505a5c7ee5676362c836a0bff438ed0bb21
sha256:48cf7cb63bd049cfe2f92f9160e0b47f90ba2ff2fec6af94548ee888602efea5

Both runs are deterministic, and they still differ: the second hashes a wrapper, not the text the agent receives. Only the first form is the terms binding, because it covers exactly the bytes carried in terms.text.

Common mistakes

Mistake Result
Hashing a pretty-printed JSON string that contains the text Different bytes, different digest. Hash the terms.text value itself.
Extracting text from HTML, then hashing Whitespace and entities differ from the source text.
Trimming or normalizing line endings The agent hashes what it received, so the digests diverge.
Hashing a summary while publishing the full text The agent cannot reproduce the digest.
Editing the text under the same version Old assertions keep the old hash and fail with tos_hash_mismatch.

When the terms change

  1. Write the new complete text.
  2. Pick a new terms.version.
  3. Compute the new hash from the new bytes.
  4. Publish text, sha256, version and url atomically.
  5. Expect 409 tos_hash_mismatch for assertions that carry the old hash. The agent re-discovers, re-reads and re-accepts.

Do not keep two hashes in one discovery document. A grace period, if you have one, is documented outside discovery and applies to assertions already in flight, not to the advertised value.

Verify

Run the validator against the document that contains the text:

pnpm --silent validate:discovery https://example.com/.well-known/agent-signup.json --json

A passing run reports valid: true and no field.terms.sha256 error. See the conformance validator.

Next

sections

type to search.

up/down navigateenter selectesc close