Evidren docs

Reference for Evidren's engine and command line. The output blocks are captures of runs against packages/cli/dist/bin.js at product commit e6b53e2.

To install Evidren, use the downloads. This page is for developers who build Evidren from its source tree. What Evidren does not guarantee: How it works.

Build from source.

Node 22 or newer is required. There is no native build step: the record's index uses the node:sqlite inside Node.

Without the alias, substitute node <evidren>/packages/cli/dist/bin.js for evidren in every command on this page.

EVIDREN_HOME must be on a local disk, not NFS or SMB: the append lock relies on local filesystem atomicity to serialize the hook and the gateway.

shell 12 packages
# 1. Build. One pnpm workspace.
cd signat
pnpm install
pnpm run build

# 2. Make `evidren` runnable from anywhere.
alias evidren="node $(pwd)/packages/cli/dist/bin.js"

# 3. From your project directory.
cd /path/to/your/project
evidren init
evidren status

Fig. 1: the whole install, before publication. Evidren itself comes from no registry; pnpm install fetches its dependencies from npm.

What evidren init does.

Everything it does is local. Nothing is sent anywhere.

The home. $EVIDREN_HOME (default ~/.evidren, or ~/.signat for an install made under the previous name) gets redaction.key, 32 random bytes at mode 0600, telemetry.json with both streams off, and a genesis chain head, so erasing the record and its seal on an initialized home fails loudly.

The wiring. Every assistant whose binary is on PATH is wired. For Claude Code, hooks are merged into <project>/.claude/settings.json. If that file is not valid JSON, init leaves it alone and warns; if it sets allowManagedHooksOnly, init warns that enterprise policy may block project hooks.

The manifest. What was wired is stamped into the chain as a coverage manifest, which status and verify read back. A home never stamped reports coverage: unknown.

The probe. Init drives the installed hook through a real shell with an rm -rf / payload and requires a deny. That proves the hook denies, not that the assistant calls it, and init says so: check evidren log after a real agent session.

The consent text. The exact JSON each telemetry stream would send is printed. Both stay off until evidren telemetry on.

Evidren decides only on the tool calls it is registered for. Everything else runs unmonitored and leaves no line in the record. What it sees, call by call.

CLI reference.

--home <dir> overrides the Evidren home for any command. --json takes no value; every other --flag consumes the next token.

Exit code 1: verify on a failed chain or pack, evidence without --out, and an unknown command.

Command Does
evidren init [--project <dir>] Create the home, wire every assistant on PATH, stamp the coverage manifest, fire the probe, print the consent text
evidren status [--project <dir>] Home, key, hooks, chain, coverage, telemetry
evidren log [--severity <s>] [--type <t>] [--session <s>] [--action <a>] [--limit <n>] [--json] Recent lines, from the SQLite index. Default limit 50
evidren policy The effective policy here, plus any rejected repo overrides
evidren verify [--pack <dir>] Check the chain and the tail seal, or an exported pack, and print what that does not prove
evidren shadow [--profile <f>] [--onunmatched <a>] [--json] Replay the record against a candidate policy. Reports only
evidren card A shareable incident card: the rule and the time, no command, path or argument
evidren evidence --out <dir> [--json] Export an Evidence Pack
evidren gateway config <path> [--allowlist <a,b>] Route an MCP client config's servers through the gateway
evidren gateway run Run the gateway. Reads EVIDREN_GATEWAY_SPEC
evidren telemetry status | on <stream> | off [stream] | rotate-id Inspect or toggle the two streams
evidren help The above

Fig. 2: the command column is what evidren help prints at commit e6b53e2.

captured session commit e6b53e2
$ evidren status
home: /tmp/evidren-cap-K6Koal/.evidren
redaction key: present
hooks: not installed
audit: 6 lines, chain OK
telemetry: usage: off, threat-intel: off, pending events: 0

coverage (stamped 2026-09-21T09:23:40.458Z):
  claude-code
    before execution: Bash|Write|Edit
    results checked:  Bash|Write|Edit|Read
  MCP tool calls:   NOT routed — run `evidren gateway config`
  Any tool not listed above executes without a verdict and leaves no line. MCP tool calls are covered only after `evidren gateway config` repoints the client. A missing line is not detectable from the chain.

$ evidren policy
onUnmatched: allow
no-remote-pipe-shell          deny              when: type=shell matches=curl.*\|\s*(bash|sh)
force-push-needs-approval     require_approval  when: type=shell matches=git push.*(--force(?!-with-lease|-if-includes)|-f)\b
block-nonexistent-packages    deny              when: type=package_install detector=deps finding=nonexistent
redact-secrets-outbound       redact            when: type=mcp_tool_call detector=secrets

$ evidren log
#1  2026-09-20T09:21:00.640Z  allow             shell           claude-code   sev:-  session:site
#2  2026-09-20T09:21:00.727Z  deny              shell           claude-code   sev:critical  session:site
#3  2026-09-20T09:21:00.799Z  require_approval  shell           claude-code   sev:-  session:site
#4  2026-09-20T09:21:00.866Z  redact            file_write      claude-code   sev:high  session:site
#5  2026-09-20T09:21:00.932Z  allow             shell           claude-code   sev:-  session:site

Fig. 3: real output from a throwaway setup, not a real install. The log lines were recorded for this page on 20 September 2026.

The record.

One JSONL line per decision, appended and never rewritten, at $EVIDREN_HOME/audit/audit.jsonl. The index.db beside it is a rebuildable node:sqlite index for evidren log; the JSONL is the source of truth.

Each line carries the sha256 of the line before it, and audit/chain-head.json seals the tail. A recognised secret is stored as a keyed reference, an unrecognised one as written. Formats: Record format.

Retention: none. The file grows. Evidren does not rotate, trim or forward it.

One chain, two processes

The hook and the gateway append to one chain through an exclusive lockfile with an ownership token; a crashed holder's lock is stolen only behind a double stat guard. If the lock cannot be taken, the append throws rather than forking the chain, and the verdict stands: a deny survives an unwritable audit home.

What evidren verify prints

Success prints its own boundary every time: unaltered and untruncated, yes; complete, unknowable from the file alone. What a verified chain proves.

Failure names the first link that no longer matches, the line after the edit: audit chain FAILED: prev-hash-mismatch at line 3. On a home the reason is one of ten: prev-hash-mismatch, unparseable-line, chain-head-missing, chain-head-missing-on-initialized-home, chain-head-unparseable, chain-head-mismatch, chain-head-hmac-invalid, genesis-chain-head-hmac-invalid, redaction-key-missing, chain-head-present-but-log-empty.

The Evidence Pack.

evidren evidence --out <dir> writes seven files and prints a sha256 for each of the five content files, plus one for the manifest. The manifest sha256 is the single value to record out of band, somewhere the exporting machine cannot reach; it fixes the pack contents at export time.

evidren verify --pack <dir> performs the same chain check from the pack's own bytes. It does not check the tail seal, and it says so. What an outsider can check without Evidren: the verifier.

File Contents
audit.jsonl The recorded chain, byte for byte, with the payloads masked at decision time
chain-head.json The tail seal: {seq, headHash, hmac}
policy.json The policy as resolved at export time
summary.json Counts only, by verdict, action type and severity; redaction references; first and last timestamp; chain status at export
shadow-report.json The counterfactual under the proposed stricter profile, labelled in the file as not in force
manifest.json Format, Evidren version, line count, chain status, a sha256 per content file, and externallyVerifiable: which checks an outside party can and cannot run
VERIFY.md A runnable recipe for rechecking the chain without Evidren, and the pack's own statement of what it does not prove

Fig. 4: a broken chain is exported too. summary.json and manifest.json then carry the failure reason, and the CLI says so.

evidren shadow measures the quiet posture.

The shipped default is onUnmatched: allow. shadow replays the record against a candidate policy and reports what would have been decided differently. It enforces nothing and changes no config.

The bundled policies/shadow-strict.yaml adds nine rules proposed on July 31, 2026 to the 4 shipped ones. Two have since shipped as detector rules, staged-remote-exec and evidren-self-tamper; the other seven are drafted, not in force and not signed off. --profile <file> replays your own YAML instead.

On the July 31, 2026 corpus the seven would have stopped or queried 16 of the then remaining 25 silent allows; the corpus has since grown to 81 cases with 24 silent allows, and that replay has not been rerun. Across 40 ordinary developer commands the profile interrupts 4, all require_approval; the shipped default interrupts none.

evidren shadow, final lines printed every time
WHAT THIS DOES NOT SHOW (do not read absence as safety):
  - actions never routed through Evidren are not in the log
  - only policy rules replay; a new DETECTOR’s findings were never recorded
  - payloads are the redacted ones the audit stored (D19)

Fig. 5: every run ends by printing its own blind spots.

The MCP gateway.

A second, separate step. The Claude Code hook does not cover MCP tool calls; to cover them, you route them.

evidren gateway config path/to/mcp-config.json

Every entry under mcpServers is rewritten to launch through the gateway; the original command and args travel in EVIDREN_GATEWAY_SPEC. The file is first copied to <path>.bak. If that exists, even as a planted dangling symlink, the next free .bak.N is used, and the copy refuses to write through it.

Once routed, the gateway evaluates the server's tool list on connect (poisoning, hidden unicode, definition drift between sessions), each outbound tool call (secrets in arguments, destination allowlist) and each inbound result (indirect prompt injection, secrets). A deny fails the tool list. A require_approval or redact finding there is recorded and the list still passes, so a drift alarm is a line in the record, not a block.

The upstream process gets the SDK's safe defaults, the server's own declared env and whatever you name in --allowlist, never your full process.env. Six loader variables are removed even if env or --allowlist names them: NODE_OPTIONS, LD_PRELOAD, LD_LIBRARY_PATH, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH.

Redacted results are rebuilt from scratch, masked text and isError only, so structuredContent and any extra fields cannot pass through unmasked.

Three layers merge, weakest first.

  1. Built in defaults. policies/default.yaml, compiled in.
  2. $EVIDREN_HOME/config.yaml, trusted. May do anything: set defaults.onUnmatched, redefine rules, disable rules.
  3. <repo-root>/.evidren.yaml (or .signat.yaml from before the rename), untrusted. Tighten only. Found by walking up from the working directory.

What the repo layer may not do

The repo layer is the one a hostile repository controls. It may add a rule with a new id whose action is not allow, and it may neutralize a require_approval rule. It may not set defaults.onUnmatched, redefine an existing rule, add an allow rule, or allowRules or disableRules a deny or redact rule.

An unparseable config file is treated as absent rather than crashing the pipeline.

printed by evidren policy recorded as an event
TRUST VIOLATION: repo config attempted to redefine existing rule "no-remote-pipe-shell"; ignored

Fig. 6: every rejected attempt is dropped, recorded in the chain and printed.

A committed .evidren.yaml cannot weaken a deny. An agent that can write $EVIDREN_HOME/config.yaml, .claude/settings.json or the audit directory can disarm Evidren: the limits.

The policy file.

action is one of four verdicts. Every when key present must match; an empty when matches everything. A rule missing a valid id or action is dropped, not fatal.

allow require_approval redact deny
policies/default.yaml first rule, verbatim
version: 1
defaults:
  onUnmatched: allow
rules:
  - id: no-remote-pipe-shell
    when: { type: shell, matches: 'curl.*\|\s*(bash|sh)' }
    action: deny
    severity: critical
overrides:
  allowRules: []      # downgrade a rule to allow
  disableRules: []    # remove a rule entirely

Fig. 7: the shipped default is 4 rules and onUnmatched: allow. A test asserts it stays that way, so a proposed rule cannot drift into enforcement unnoticed.

when key Matched against
type the normalized action type
matches for type: shell, the command string; otherwise JSON.stringify(payload). Unanchored, case sensitive RegExp. A pattern that does not compile never matches
detector a finding from that detector is present
finding a finding with that code is present

Fig. 8: the four match keys, and nothing else.

Evaluation order, strictest first

  1. an explicit deny rule
  2. a critical finding suggesting deny
  3. an explicit require_approval rule
  4. a finding suggesting require_approval
  5. defaults.onUnmatched, only when nothing matched

Then the redaction modifier. If the gate is allow and any finding produced concrete redaction targets, the verdict becomes redact. A redact gate with no targets escalates to require_approval and attaches a redaction-incomplete finding, because a redaction the record cannot execute would store the payload in clear.

redact-secrets-outbound is scoped type: mcp_tool_call, which only the MCP gateway emits. On actions the hook covers, masking comes from the redaction modifier, not from that rule.

Telemetry. Two streams, both off.

No endpoint is configured, so even an enabled stream sends nothing. evidren telemetry off disables both streams at once.

usage

A random install UUID, never tied to machine or identity and rotatable with evidren telemetry rotate-id; Evidren version; OS and arch; Node version; surface; decision counts. Nothing else. Batched at most once a day, state in telemetry-usage.json.

threat-intel

Only on a deny in a known bad class: deps findings coded nonexistent or security-hold, and critical command-risk findings coded remote-pipe-*. known-malicious is reserved in the filter, but no detector emits it today. The token is the package name, or for a command a sha256 of the normalized signature, never the command line. Queued in telemetry-queue.jsonl.

threat-intel event the whole payload
{"t":"threat-intel","installId":"ae7b9b4e-72ed-49e0-963e-f62b5b2cca2a","code":"nonexistent","severity":"high","ecosystem":"npm","token":"left-pad-utils"}

Fig. 9: the example printed by the consent text at evidren init, byte for byte.

Never sent: allow, ask and redact events; file contents; paths; repo names; environment variables; secrets; tool arguments; raw command lines.

Three filters run before anything is queued. The event is dropped if the record's masking pass would change a single byte of it, compared as is and again case folded. Its shape is validated field by field. A package name token of 16 characters or more with a Shannon entropy above 3.5 bits per character is dropped, so a mistyped npm i AKIA… cannot be shared as a package name.

Build, test and gates.

The gate scripts drive the built artifacts in dist/, not source, so they catch what a source level suite cannot see: entry guards, code splitting, hook quoting. gate-m4.mjs runs 19 checks, among them that a deny survives an unwritable audit home and that evidren verify fails on a truncated tail.

shell build order matters
pnpm install
pnpm run build      # before typecheck: @evidren/cli typechecks against the others' built .d.ts
pnpm run typecheck
pnpm run lint
pnpm run test

node scripts/gate-m1.mjs
node packages/mcp-gateway/gate-m3.mjs
node scripts/gate-m4.mjs

Fig. 10: gate-m2.mjs stays out of CI on purpose: it queries a live package registry, so in CI it would test the network, not Evidren. It runs locally at release time.

CI

CI builds and tests on macOS, Windows and Linux, on Node 22 and 24, and runs the offline gates. The Windows installer and the Mac disk image on the download page are built there.

12 packages

@evidren/core (policy engine, config, audit chain, telemetry), @evidren/detectors, @evidren/claude-code (hook and installer), @evidren/copilot-cli (the GitHub Copilot CLI hook), @evidren/mcp-gateway, @evidren/integrations (one entry per agent tool, with what is verified), @evidren/product (lifecycle, status, record and views), @evidren/cli, and two front ends: evidren-vscode (a thin editor binding) and desktop (the macOS app, which ships its own interpreter). All 12 are private: true: none is on a package registry.