SANS FIND EVIL! submission — the inhuman team, powered by here.build.
A purpose-built MCP server that turns any agent host into a forensic analyst who cannot fabricate a finding and cannot modify the evidence — because neither is a state the architecture can represent. We demo Claude Code driving it to autonomously solve a published Windows intrusion (DFIR Madness case001, "Stolen Szechuan Sauce").
See docs/SUBMISSION.md for the full writeup and the mapping to the
six judging criteria.
The #1 fear of AI-driven IR is the model summarizing raw data into a fabricated finding. Every other approach fights this with prompts ("only report what you can confirm"), graders, and confidence scores — mitigations on an output that can still lie.
This server removes the failure mode at the wire. It exposes one tool,
sift_discovery(intent, expr), and every result it returns is sealed: a value signs only
if it was read from a real evidence row. A value the host typed into its query does not
sign — even when it's correct:
the real C2 IP, READ from a Volatility row → "203.78.103.109" signable: TRUE
the real C2 IP, TYPED into the query → "203.78.103.109" signable: FALSE
Same answer-key value — it signs when read, refuses when written. The only path to a signed finding is a tool execution against hash-verified evidence. (The same move Delta makes for CSS — wrong states unrepresentable — pointed at forensic grounding.)
The agent host and the evidence are separated by a single grounding membrane: this MCP server.
┌─ AGENT HOST (Claude Code) ──┐ stdio MCP ┌─ INHUMAN MCP (the membrane) ──┐
│ has the model │ ──intent+expr─►│ has the evidence tools │
│ reasons · plans · self-fixes │ │ has NO model │
│ no RAW evidence — │ ◄─sealed result┤ seals every result │
│ only sealed projections │ + custody │ NO destructive verb exists │
└──────────────────────────────┘ └───────────────┬────────────────┘
│ read-only (:ro)
┌───────────────▼───────────────┐
│ frozen evidence image · │
│ SHA-256 custody · Docker │
└─────────────────────────────────┘
The host is external and untrusted — Claude Code, another model, or your own client. The guarantee does not depend on which:
- It cannot modify the evidence. The only verb on the wire is
sift_discovery. There is noexecute_shell, nowrite, nodelete— a destructive command isn't a thing the host can name. The image is bind-mounted read-only. - It cannot fabricate a signable finding. The host writes the query, so it can type a guessed value — but the seal refuses to sign any value that wasn't read from a row (the §2b typed-literal gate). Fabrication isn't caught by a grader; it is structurally unsignable.
- Both hold against any host, because the guarantee lives in the server, not in our orchestration of it. A frontier agent we don't control cannot fabricate or destroy against this membrane — which is a stronger claim than "our own loop behaves."
One command — proves the seal, runs the full offline gate, then tells you what's next:
./try-it.shOr step through it by hand:
# Prove the seal — a READ signs, a TYPED value does not (no Docker, no model, no downloads):
SIFT_MOCK_EVIDENCE=1 node --experimental-strip-types mcp/src/cli.ts --probe
# The gate — the full offline suite (no Docker, no model):
pnpm --filter @sift/mcp --filter @sift/evidence smoke
# Drive one grounded query by hand, with provenance:
SIFT_MOCK_EVIDENCE=1 node --experimental-strip-types mcp/src/cli.ts '(socket/netscan)'Connect Claude Code (the headline) — or any MCP host. The server speaks stdio MCP on the
official SDK; point a host at mcp/src/server.ts. Add to your project .mcp.json (Claude Code)
or claude_desktop_config.json (Claude Desktop):
{
"mcpServers": {
"sift": {
"command": "/ABSOLUTE/PATH/TO/node",
"args": [
"--experimental-strip-types",
"/ABSOLUTE/PATH/TO/inhuman-sift/mcp/src/server.ts"
],
"env": { "SIFT_EVIDENCE_ID": "/cases/dc01/memory.mem" }
}
}
}
⚠️ Use an absolute path tonode(≥ 22.6, for--experimental-strip-types), not bare"node"— MCP hosts launch servers with a minimalPATHthat omitsnvm/volta/fnmshims. No build step: the server runs.tssource directly. For a no-Docker trial dropSIFT_EVIDENCE_IDand set"env": { "SIFT_MOCK_EVIDENCE": "1" }(bundled case001 fixtures). All diagnostics go to stderr — stdout is the JSON-RPC transport.
Evidence images are public downloads, registered locally — see EVIDENCE.md for
the judge's on-ramp (URLs, registration, expected timings).
| Layer | What runs |
|---|---|
| Substrate | @here.build/arrival-scheme — real born-provenance, promise-parallelism |
| Memory | Volatility 3 (pslist, netscan, malfind, …) in Docker |
| Disk | Sleuth Kit (carve) · Plaso (super-timeline) · regipy (registry/amcache) · python-evtx (event logs) · analyzeMFT |
| Evidence | DFIR Madness case001 — DC01 memory + disk; DESKTOP; MemLabs 1–6 — registered, SHA-256-verified |
| Model | supplied by the host — we demo Claude Code; the server is model-agnostic |
Six evidence families, nine pure reference families, ~51 forensic functions. The forensic values
are typed entities (SchemeIP, SchemeDateTime, Hash, Path, …) that thread provenance
through pure operations — externality is derived, timestamps are comparable instants
(Fantasy-Land Ord), never locale-fragile string sorts.
Every guarantee is enforced by STRUCTURE. The "prompt-based" column is empty on purpose.
| Guarantee | How it's enforced | Kind |
|---|---|---|
| Host cannot modify the evidence | The only wire verb is sift_discovery; no write/delete/shell exists; image bind-mounted :ro |
Architectural (absent verb + RO mount) |
| Host cannot fabricate a signable finding | The seal refuses to sign any value not read from a row — a typed/guessed value is unsignable (§2b) | Architectural (the seal) |
| Host cannot reach an ungranted family | The grant constructs the env; ungranted tools are absent — calling one is an unknown-symbol error, not a denied permission | Architectural (structural absence) |
| Evidence cannot be silently altered | SHA-256 hashed on first access, stamped into every custody entry + cache key; a re-registration mismatch is refused | Architectural (custody-by-hash) |
| Prompt-based guardrails | none — this row is empty by design | — |
A fabricated finding is not caught — it is unconstructable. There is no "only report what you can confirm" prompt, no after-the-fact grader, no confidence threshold.
- Per-finding, not per-verdict. Each signable finding is read-derived, custodied, and re-runnable. The host synthesizes findings into a verdict — that synthesis is the host's reasoning (transparent in the execution log), and every atom it rests on is independently re-runnable. The mechanical stage that decomposed a verdict into audited sub-claims lives in our reference driver (a separate development repo, not part of this submission).
- Evidence integrity is custody-by-hash, not re-hash-per-read. The image is hashed on first access and that SHA is stamped into every custody entry and cache key (re-hashing a multi-GB image on every read would be wrong engineering). A mismatch at re-registration is refused.
- A local 8-bit model also solves the case through the same server — $0, no cloud — via our
reference driver (a small-model fan-out:
rnjscouts and writes the queries, larger models ideate; it found the evil in under 5 minutes). Lifting it onto the pure wire path is future work. - Three execution-family artifacts (prefetch / userassist / srum) are not yet wired to a real parser and return empty under real evidence; the offline suite exercises their mock rows — nothing fabricates.
inhuman-sift/
├── mcp/ @sift/mcp — THE SUBMISSION: the discovery membrane + the stdio MCP server
│ ├── src/server.ts the stdio MCP endpoint a host connects to (tools/list → tools/call)
│ ├── src/discovery-tool.ts sift_discovery — describe() + call(intent, expr), the sealed surface
│ ├── src/discovery.ts runDiscovery + sealVerdict — the grounding seal (signable/scoped/unsigned)
│ ├── src/discovery-env.ts the family/grant roster (capability = structural absence)
│ ├── src/entities/*.ts typed forensic values (IP, DateTime, Hash, Path, …)
│ └── src/cli.ts drive one grounded query by hand, with provenance
│
├── evidence/ @sift/evidence — THE SUBMISSION: the substrate (zero workspace deps, node builtins)
│ ├── src/docker-exec.ts the Dockerized forensic runners (Vol3 / TSK / Plaso), read-only mount
│ ├── src/ledger.ts the append-only chain of custody (each read carries its `intent`)
│ ├── src/evidence-paths.ts the path resolver + SHA-256 custody hashing
│ └── src/{cache,plaso,tsk,extract,artifacts}.ts tool plumbing + result cache
│
└── docs/ SUBMISSION.md · submission/{ARCHITECTURE,ACCURACY,DATASET,DESCRIPTION,TRY-IT-OUT} · examples/
The submission ships @sift/mcp + @sift/evidence. They depend strictly downward
(mcp → evidence), and neither imports a model — that one-directional, model-free dependency is
what makes "the host cannot fabricate" a property of the artifact rather than a promise about our code.
Dependencies. The membrane is built on the arrival framework (@here.build/arrival, -mcp,
-provenance, -serializer, -sweet, …) — a separate open-source project; pnpm install resolves it
from there. Everything else is standard npm (@modelcontextprotocol/sdk, zod, yaml). A multi-agent
reference driver (the speed layer — qwen · rnj · glm · opus) lives in our development repo; it is
not part of this submission.
A separate project on top of the arrival architecture, not part of here.build proper — its own thing.