Skip to content

Claude Code integration evidence

Seal’s protected path runs through Claude Code: seal protect installs a local override with claude mcp add, and Claude Code is the process that starts the Seal proxy, renders the approval request and returns the human’s answer. The repository’s automated tests exercise the protocol, the approval state machine and the proxy against scripted MCP clients. No test in this repository observes the real client doing any of it.

This page is the client matrix, the acceptance run that closes the gap, and the machinery that makes such a run checkable instead of anecdotal.

This checker establishes that a recorded run is internally consistent, that its inputs were present and readable, and that the pack was not casually relabelled. It does not establish that a real Claude Code process produced it. A determined author with local file access can produce a passing pack. This is an instrument against mistakes, not against forgery.

Binding is bookkeeping, not a control. The recorder digests live in the run’s harness-state.json, alongside the recorder sources and casts. Someone who can rewrite that run directory can rewrite all of them consistently and produce a passing pack. The binding detects accidental edits and mismatched files; it does not stop that same writer from forging the bundle.

The harness also cannot establish that a human rather than the client originated the decline. It reads the recorder-corresponding terminal output history for the dialog text and correlates the declined call with the proxy’s receipts, but the interactive client remains inside the declared approval-origin boundary. This limit is repeated in every pack’s manifest.json.

The Claude Code matrix row stays UNTESTED — real Claude Code call not observed until an operator’s real run fills it. That row is the honest claim; the checker’s exit code is not.

Client What is exercised Status
Scripted stdio MCP client (test/spine-retry.test.cjs, test/protect3b.test.cjs, test/four-beats.test.cjs) Protocol, retry continuation, one-use consumption, protect/unprotect state machine TESTED — runs in CI on every push
Claude Code Override selection, dialog rendering, answer return, no fallback UNTESTED — real Claude Code call not observed

The matrix gates on wiring and protocol behaviour. It deliberately does not carry a “no alternate-route success” row. Whether a model reaches the same effect through Bash, gives up, or invents an answer is model behaviour, and Seal states plainly that it is a gate and not a sandbox: routes that do not pass through the sealed path were never controlled. A behavioural probe can record what one model did on one day; it cannot establish a product guarantee, so it must not gate a release.

A human-assisted, instrumented acceptance run. The supported product is interactive, so a human performs the three irreducible acts — issue the instruction, inspect the dialog, accept or decline. Every effect-level fact is established by a machine and written to a file while it happens.

A scripted client cannot substitute. It proves the protocol, the state machine and the proxy, and this repository already holds strong evidence there. It cannot prove that Claude Code selects the local override, renders the elicitation, or declines to fall back.

  • Node.js 20 or later, and util-linux’s script command. Confirm both before starting; script records each terminal session that the harness checks:

    Terminal window
    $ node --version
    $ node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 20 ? 0 : 1)'
    $ script --version
  • One frozen Seal artifact, identified by SHA-256, byte length and the installed-tree digest the installer records.

  • One exact Claude Code version, plus the SHA-256 of the client executable. The harness uses the explicit --client path when the human gives one. Otherwise it resolves every executable claude entry on PATH. It continues only when those entries resolve to one real path. It refuses ambiguous client paths and lists each path and SHA-256.

  • Linux x86-64.

  • A clean temporary HOME, XDG_DATA_HOME, XDG_CONFIG_HOME and project.

  • seal doctor reporting no elicitation auto-response hook; with one configured, the harness refuses to start, because human approval origin could not then be claimed.

  • A purpose-built MCP fixture (harness/claude-code/fixture-server.cjs) that appends every frame it receives to an fsynced append-only log, records the process ancestry it was launched under, and writes one child-call record per guarded call.

Case Required observation
activation After restart, Claude Code selects the local Seal override
negotiation The proxy records the retry-model interaction
approval_shown Recorder-corresponding terminal output history contains the complete exact-call dialog; the published pack does not include the raw cast for an independent display check
before_approval Child call count remains 0
accept Child call count becomes exactly 1; expected effect hash matches
decline Child call count remains 0
missing_launcher Claude Code does not fall back to the original .mcp.json server
unprotect The local override disappears and .mcp.json remains byte-identical

This table summarizes each requirement because the literal requirement strings also name the file-backed facts that support the observation. The test renders this table from the harness cases. The test rejects a document that differs from that rendering.

For approval_shown, “complete exact-call dialog” means the complete set of strings derived from the installed artifact: its first three message lines (excluding a standalone generic Approval required title, if present), schema title, and full schema description. The current renderer supplies the tool, argument value, and scope in those first three lines for append_note; when known, it also supplies the configured (not authenticated) server route within the same seven-line message budget. Its description repeats the argument and full scope, including TTL, and adds Outside Seal: Bash, network, subprocesses, other tools and servers.

The historical Claude Code 2.1.251 frame paints Tool: append_note, a bare Arguments: label, the schema title Approve one run: append_note, and the old description Arguments: note: seal-accepted-note. Scope: at most one run. It folds the other three message lines as … (+3 more lines). That unchanged recording establishes the painted channels, but cannot certify the new wording; the exact-client acceptance row remains untested.

How each one is established from files rather than from the operator’s memory:

  • activation — the fixture’s start record carries its process ancestry. The run requires a chain of claudeseal __proxy --protect-state … → fixture, requires that proxy’s pid to equal the lease pid Seal wrote into its own protection state, and requires that no fixture process started with any other parent.
  • negotiation — the proxy’s own receipts: an INPUT_REQUIRED receipt and a later receipt carrying the same approval correlation, with the matching issued and consumed entries in the fsynced approval journal.
  • approval_shown — the installed artifact’s own renderer supplies the first three message lines and its contract supplies the schema title and description. The harness looks for those strings together in the recording, including the full description’s scope, TTL, and boundary for the current artifact. Its permitted span is derived from the complete source message plus schema title and description. The cast must also match its recorder-written digest and be the deterministic asciicast conversion of the same session’s raw output and advanced timing files. A substituted text-only cast therefore refuses even if it contains every expected string.
  • before_approval / accept / decline — child-call records counted out of the append-only log, plus the effect digest, which is computed three ways that must agree: by the fixture as it wrote the file, by the harness from the instructed note, and by the checker from the pack alone.
  • missing_launcher — the harness moves the override’s command aside. The session’s rendered transcript must carry seal-fallback-note or does not fall back. The child log must gain no record at all: not a Seal-started one, and not a directly started one. .mcp.json must be unchanged, and the installed tree must re-verify after the launcher is restored.
  • unprotect — the override entry is gone from ~/.claude.json and from claude mcp get, and .mcp.json matches the digest and byte length recorded before seal protect ran. seal unprotect notes must also exit zero and report that the sealed MCP route is outside Seal. A pre-existing absent override is not evidence that this act occurred.

Use this procedure and the harness from current main, even when accepting an older artifact; tagged copies of this page can predate the SEAL_VERSION prompt. Do not check out the artifact tag to follow this walk.

First download one published Linux x86-64 artifact and the SHA256SUMS asset attached to that same GitHub release. Set SEAL_VERSION to the release tag being accepted, chosen explicitly from the published releases. Enter that exact tag at the prompt below, including when accepting an older release; do not substitute the latest release. These commands obtain the frozen artifact and set the digest and length used by init; they also verify that the downloaded bytes match the release’s checksum record.

Choose a new, empty run directory outside every Git working tree, and enter its absolute path at the second prompt (for example, a new directory directly under your home directory, if your home is not a Git working tree). Do not put it inside the Seal clone: Claude Code keys its local MCP configuration to the Git root, while the harness looks up the exact <run-dir>/project path. A mismatch leaves the harness’s local override evidence absent and activation refuses. mkdir creates the new directory; the following guard rejects a Git working tree before init. Do not reuse a directory from an earlier run. The harness creates its clean temporary HOME, XDG directories, and project beneath that run directory. Verification, permission change, and harness initialization are one POSIX shell command: a failed comparison skips them. Read the sign-in, MCP-scope, workspace-trust, and manual-mode instructions below before executing this block.

Terminal window
printf '%s' 'Published release tag (for example vX.Y.Z): ' && read -r SEAL_VERSION && test -n "$SEAL_VERSION" && SEAL_ARTIFACT="seal-${SEAL_VERSION}-linux-x64" && curl -fsSLO "https://github.com/velvetmonkey/seal/releases/download/$SEAL_VERSION/SHA256SUMS" && curl -fsSLO "https://github.com/velvetmonkey/seal/releases/download/$SEAL_VERSION/$SEAL_ARTIFACT" && SEAL_SHA256="$(awk -v name="$SEAL_ARTIFACT" '$3 == name { print $1 }' SHA256SUMS)" && SEAL_BYTES="$(awk -v name="$SEAL_ARTIFACT" '$3 == name { print $2 }' SHA256SUMS)" && test -n "$SEAL_SHA256" && if command -v shasum >/dev/null 2>&1; then actual_sha256="$(shasum -a 256 "$SEAL_ARTIFACT")"; else actual_sha256="$(sha256sum "$SEAL_ARTIFACT")"; fi && test "${actual_sha256%% *}" = "$SEAL_SHA256" && actual_bytes="$(wc -c < "$SEAL_ARTIFACT")" && test "$actual_bytes" -eq "$SEAL_BYTES" && chmod +x "$SEAL_ARTIFACT" && printf '%s' 'New absolute run directory outside every Git tree: ' && read -r run_dir && case "$run_dir" in /*) true ;; *) printf '%s\n' 'Run directory must be absolute.' >&2; false ;; esac && mkdir "$run_dir" && command -v git >/dev/null && if git -C "$run_dir" rev-parse --show-toplevel >/dev/null 2>&1; then printf '%s\n' 'Run directory is inside a Git tree; choose one outside.' >&2; false; else true; fi && node harness/claude-code/cc-harness.cjs init --artifact "./$SEAL_ARTIFACT" --sha256 "$SEAL_SHA256" --bytes "$SEAL_BYTES" --run-dir "$run_dir" && node harness/claude-code/cc-harness.cjs next --run-dir "$run_dir"

At the finishing prompt, run finish --out ., not another next.

next is the whole run: it takes the machine readings, prints what the human must do, launches the recorded session, and stops. Repeat it for each prompted stage through unprotect. When its final prompt offers the finishing step, do not run a final next: finish --out below replaces that final next and writes the pack straight into the current checkout. (Without --out, that final next writes the pack under <run-dir>/pack.)

Terminal window
$ node harness/claude-code/cc-harness.cjs finish --run-dir "$run_dir" --out .

Run it in a terminal at least 80 columns wide — the approval dialog is measured at 80, and a narrower terminal would wrap the effect out of the recording.

Before starting a recorded session, read these operator instructions:

  • In the fresh harness project, accept the project’s .mcp.json server when prompted. Project-scope notes names the fixture Node command; local-scope notes must name Seal’s __proxy. The two entries are expected: the local override is the protected route. In /mcp, confirm that local notes is connected; a connected project fixture alone does not establish activation.
  • If Claude Code says MCP choices apply only to this session because the workspace is not trusted, explicitly trust this disposable harness workspace if you want those choices to persist. Otherwise reaccept and verify them in each recorded session across the six steps; do not assume they persisted.
  • Before issuing either the decline or accept instruction, press Shift-Tab until the status says manual mode on, and verify it in each session, because an automatic answer compromises the human approval origin. Claude Code 2.1.233 was observed starting with auto mode on; do not let the client answer the elicitation automatically.

Two cautions for the operator:

  1. Claude Code starts in the fresh HOME at <run-dir>/home and may ask you to sign in. In the first session, use /login and complete the interactive OAuth flow before the activation instructions. Confirm that the TUI is signed in: claude auth status alone was observed succeeding while the TUI still reported Login expired. If preparing authentication beforehand, use the same harness HOME and project (<run-dir>/project). Preserve projects[<absolute-harness-project>].mcpServers.notes in <run-dir>/home/.claude.json exactly as seal protect wrote it during init. Do not copy a full operator ~/.claude.json over that file. If transferring OAuth credentials, merge only the authentication data and retain that project entry; a credentials-only copy still requires confirmation in the TUI. The raw recording is a verbatim capture of your terminal, including login input, and stays in the run directory. The pack carries a rendered transcript. Read rendered-transcript.txt before publishing it.
  2. The installed store is read-only by design. chmod -R u+w the run directory before deleting it.
<separately captured evidence-pack directory>/
<client-version>/
linux-x64/
<seal-artifact-sha256>/
manifest.json
rendered-transcript.txt
rendered-transcript-activation.txt
rendered-transcript-decline.txt
rendered-transcript-missing-launcher.txt
proxy.jsonl
child.jsonl
before-after.json
approvals.journal
receipts/
snapshots.json

There are rendered transcripts only for the four recorded terminal sessions: rendered-transcript.txt is the accept session, while the suffixed files above are respectively the activation, decline, and missing_launcher sessions. The other four cases are established from the fixture, proxy, approval, and state records, so they have no rendered transcript file.

The evidence contract is seal.claude-code-evidence/v2. The identifier lives in manifest.json.manifest. The harness writes it in harness/claude-code/cc-harness.cjs, and the checker reads it in scripts/check-cc-evidence.mjs. Version 1 packs are refused because they publish raw .cast files instead of the required rendered transcript.

manifest.json names the artifact, the client, the environment, the fixture revision, the eight expected cases with their required observations, what was observed, the renderer provenance, and the SHA-256 and byte length of every other file in the pack. The rendered transcript holds any retained scrollback followed by the terminal’s last visible frame. It is NOT a record of the whole session. The renderer keeps scrollback for synthetic casts. Real Claude Code recordings can contribute scrollback when terminal output scrolls. If a line is repainted, the transcript carries its final content once. Earlier versions of that line are lost. It removes terminal control sequences. It also redacts Claude Code session URLs, bare session_ identifiers, and UUID-shaped identifiers. Other visible text can still contain secrets or sensitive content. A ULID or a 32-character hex string is not a Claude Code session identifier by shape alone, so these forms remain visible. This pack is not safe to publish without inspection.

Terminal window
$ node scripts/check-cc-evidence.mjs <evidence-pack-directory>

This repository does not ship an evidence/claude-code/ pack. scripts/check-cc-evidence.mjs accepts a pack or refuses it by name. It holds its own copy of the eight required cases and its own copy of the label rule, so a manifest cannot tell the checker what the rules are. It refuses a file whose hash does not match, a file the manifest names but the pack does not carry, a file added beside the manifest that no hash covers, a manifest naming an artifact other than the one under release, an altered case requirement, a case that is not observed, a manifest whose child-call count its own child.jsonl contradicts, and a summary label that is not the label the observations produce. It also strictly parses the fixture’s numbered digest chain, compares the complete log to the independent final boundary digest, length, and record count in snapshots.json, and derives the client executable identity from process ancestry the fixture read from /proc while the client and Seal proxy were alive. It also reads every rendered transcript; a synthetic fixture banner in a transcript is synthetic evidence, not ignored data. It refuses a transcript that contains a Claude Code session URL, a bare session_ identifier, a UUID-shaped session identifier, or a C0/C1 control byte other than LF. A ULID or a 32-character hex string can pass this check because its text shape does not identify it as a Claude Code session.

For a release claim, the operator must additionally supply the SHA-256 of the actual Claude Code executable they independently verified (not a hash copied out of the pack):

Terminal window
$ node scripts/check-cc-evidence.mjs <evidence-pack-directory> --release --artifact-sha256 <artifact-digest> --artifact-bytes <artifact-bytes> --client-executable-sha256 <independently-verified-claude-executable-digest>

The checker compares that supplied digest to the executable identity the live fixture recorded above Seal’s proxy. Without it, a release pack is refused as client_identity_expected_absent; a file named claude or a client-reported version is not identity.

The release workflow runs it against the exact artifact it just built. With no pack for that artifact, the release states the untested row and continues; with a pack, the pack and the operator-supplied executable digest must verify or the release fails.

Claude Code <version> integration:
PASS — manually exercised on Linux x86-64 against artifact sha256 …
Not automated in CI.

That sentence claims exactly one thing: this combination of client version and artifact was exercised, once, by hand, on Linux x86-64. It is not independent assurance, it is not a CI result, and it says nothing about any other Claude Code version. Until such a pack exists and verifies, the row reads UNTESTED — real Claude Code call not observed.

The synthetic run, and why it can never be mistaken for a real one

Section titled “The synthetic run, and why it can never be mistaken for a real one”

harness/claude-code/synthetic-run.cjs drives the entire harness with a scripted stand-in (harness/claude-code/synthetic-client.cjs) so the instrument itself is exercised on every CI run: the real artifact is installed, seal protect runs, and the real proxy gates a real fixture. Its --run-dir must be empty when opened; the runner creates its artifact and stand-in inputs in the sibling directory <run-dir>-inputs. For example:

Terminal window
$ synthetic_run_dir="$PWD/synthetic-acceptance"
$ mkdir "$synthetic_run_dir"
$ node harness/claude-code/synthetic-run.cjs --run-dir "$synthetic_run_dir"

The synthetic runner prints one Check it: command for the pack it wrote. It does not run the checker itself. Run these two commands to show both outcomes: the first accepts the synthetic pack only with the explicit opt-in, and the second intentionally refuses it without that opt-in.

Terminal window
$ node scripts/check-cc-evidence.mjs "$synthetic_run_dir/pack/evidence/claude-code" --allow-synthetic
$ node scripts/check-cc-evidence.mjs "$synthetic_run_dir/pack/evidence/claude-code"

It proves the harness works. It proves nothing about Claude Code — a stand-in that declines to fall back declines because it was written to.

The four labels below remain useful warnings, but release refusal no longer derives realness from them. The fixture hashes the actual files named by live process ancestry; the checker requires the manifest’s client executable above the Seal proxy and recognizes the checked-in stand-in’s digest there. Deleting all the labels therefore cannot turn the observed stand-in process into Claude Code:

  1. synthetic: true in the manifest, with a synthetic_banner;
  2. a SYNTHETIC-NOT-A-REAL-RUN.txt file beside the manifest;
  3. the client version 0.0.0-synthetic-stand-in, which is also the directory the pack lives in;
  4. a banner record inside the derived proxy.jsonl.

Removing the warnings does not launder the pack: the raw fixture log still commits to the stand-in executable digest observed above the Seal proxy, so the checker derives synthetic provenance and refuses the pack as synthetic_pack_in_release_evidence. The synthetic run is never written into evidence/; it goes to a temporary directory, and test/cc-evidence.test.cjs fails if any pack appears in this repository at all. Committing a real pack is the deliberate act of the person who performed the run, in a commit that changes that test and this page’s status row together.

Previous: Version identity. Up: Assurance. Next: Current guarantees and limits.