skarn(1)

AI coding session security scanner with built-in session search

skarn 0.19.0

Real-time guard: every hook event it gates, per host, is documented in the guard hook-event reference.

Synopsis

skarn check [options]
skarn assess [options]
skarn vet [options]
skarn baseline <audit|accept> <file> [<fingerprint>] [options]
skarn search <query> [options]
skarn recent [options]
skarn restore <session>
skarn messages <session> [options]
skarn stats [options]
skarn tools [options]
skarn mcps [options]
skarn cmds [options]
skarn export [options]
skarn guard [report|accept <fingerprint>] [options]
skarn setup [options]
skarn doctor [options]
skarn completion <shell>
skarn serve [options]
skarn taxonomies
skarn license [<file>|-|renew] [options]
skarn eula [accept]
skarn --version
skarn --help

Description

Skarn is an AI coding session security scanner with built-in session search. It detects leaked credentials and the attack patterns that cause or exploit them in the local session transcripts of AI coding assistants (Claude Code, Gemini CLI, Codex CLI, Cursor, and VS Code Copilot Chat), and it lets you search, browse, and analyze those sessions.

Skarn is a single static binary. It scans locally and makes no network connection by default; the only features that transmit data off the machine are opt-in and named explicitly in the SECURITY section below.

Skarn has two faces over one parsing engine. The recall commands (search, recent, stats, and friends) are the daily-use surface and never trigger a scan. The check command is the security scanner: it runs the detection rules, correlates attack chains, and computes a session risk score for CI/CD gating. Every finding is mapped to MITRE ATLAS, the OWASP Top 10 for LLM Applications 2025, and CWE; run skarn taxonomies to print the crosswalk and the pinned framework versions.

Commands

check [options]
Scan AI sessions for leaked credentials and attack patterns.
assess [options]
Scan every AI session on this machine and print a friendly risk summary, with an optional shareable redacted report. Free, no config, no license.
vet [options]
Statically vet the local AI assistant configuration (hooks, MCP servers, permission grants, plugins and skills) for risky patterns. Read-only and offline; free, no license.
baseline <audit|accept> <file> [<fingerprint>] [options]
Interactively triage findings into a committed baseline (audit), or record a false-positive fingerprint (accept).
Search past AI sessions (literal text, or --regex).
recent [options]
List recent sessions.
restore <session>
Restore a session.
messages <session> [options]
Show the messages of a session.
stats [options]
Session analytics: tokens, models, tools, and timing (text, json, csv, or html).
tools [options]
Show the tools used across sessions.
mcps [options]
Show the MCP calls made across sessions.
cmds [options]
Show the shell commands run across sessions.
export [options]
Export sessions as text, json, ndjson, or html (redacted by default).
guard [report|accept <fingerprint>] [options]
Real-time pre-execution hook for editor/agent integration; reads an event on stdin and emits a verdict. guard report summarizes the audit log, forecasts what enforcing would cost, and prints the enforce flip when the window is clean; guard accept records a flagged finding as a false positive so the guard stops blocking it.
setup [options]
Wire the skarn guard hook into detected AI coding agents (merge-based, reversible), then verify it with the guard self-test.
doctor [options]
Check whether skarn is actually protecting this machine: binary, license, wired agent hooks, guard log, session stores, and the guard self-test.
completion <shell>
Print a shell completion script for bash, zsh, or fish.
serve [options]
Start the local web UI (search, recent, stats, and a redacted scan view) bound to 127.0.0.1.
taxonomies
Print the standards crosswalk (MITRE ATLAS, OWASP LLM Top 10, CWE).
license [<file>|-|renew] [options]
Show the active license, install one from a file (or stdin), or renew it from the license service.
eula [accept]
Print the Skarn End User License Agreement, or record acceptance of the current version (accept).

Options

Check options

Control the security scan: which rules run, the scan window, output format, and CI gating. check requires a license and refuses with exit 7 before any scan without a usable one; the free license is issued at https://getskarn.com/free after a quick email confirmation and is verified offline. Only --audit-verify is exempt: verifying an audit log's hash chain needs no license.

--rules path
Load a custom TOML rules file in addition to the bundled rules.
--ai-rules path
Load an AI-specific rules file.
--no-ai-rules
Disable the AI-specific rules.
--no-default-rules
Disable the bundled detection rules.
--feed path
Apply a signed maintained-feed bundle over the baseline rules. Requires a Team license.
--update-rules
Fetch and verify the latest feed from the subscriber channel, then scan. Requires a Team license.
--feed-url url
Feed channel URL for --update-rules; falls back to $SKARN_FEED_URL. Requires a Team license.
--offline
Disable all network access (the maintained-feed fetch).
--severity level
Report only findings at or above this severity. One of: low, medium, high, critical. The default is medium.
--canary-file path
File of planted canary tokens, one per line; a match is a proven breach.
--check-packages
Flag typosquatted or URL-sourced package installs found in sessions (offline).
--check-code
Flag insecure code written by the assistant during the session.
--hours n
Scan window in hours; 0 means no time limit. The default is 720.
--cli name
Restrict the scan to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict the scan to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--session id
Scan a single session by id.
--format fmt
Output format. ndjson is an ECS-shaped event stream (one redacted finding per line) for SIEM ingest. One of: text, json, sarif, ndjson. The default is text.
-v, --verbose
Verbose output.
--no-redact
Show secrets in full instead of redacting them. Use with caution.
--no-color
Disable colored output.
--policy path
Load a declarative TOML policy file (fail thresholds, required/forbidden rules, baseline enforcement, profile); the --fail-on-* and --profile flags override it. Requires a Team license.
--profile preset
Output profile. enterprise keeps high-confidence findings and dedups them. One of: enterprise. Requires an Enterprise license.
--fail-on-severity level
Exit 1 if any finding is at or above this severity. One of: low, medium, high, critical.
--fail-on-risk 0-100
Exit 2 if the session risk score exceeds this threshold.
--fail-on-scan-error
Fail closed (exit 6) when the scan was incomplete: a tool source errored out of discovery, or a session's parse failed. Off by default (a partial scan still reports, with a stderr warning).
--baseline path
Suppress findings recorded in this baseline; fail only on new findings. A single file is free; a directory unions every .json baseline inside it (an org-shared accepted set) and requires a Team license. Without this flag, ~/.config/skarn/baseline.json is applied automatically when it exists (suppression only; --baseline-create and --baseline-merge targets stay explicit).
--baseline-create
Write the current findings to the --baseline file as the accepted set and exit.
--baseline-merge file
Consolidate the --baseline input (a file or a directory of per-member baselines) into one distributable file and exit. Requires a Team license.
--audit-log file
Append a hash-chained record of this scan (timestamp, policy, finding counts, verdict; no secrets) to this local log; detects in-place edits and reordering of recorded history. Requires a Team license.
--audit-verify file
Verify an audit log's hash chain and exit (no scan). Exits non-zero if the chain is broken. Needs no license.

Assess options

The zero-config machine-scan wedge: scan every AI coding session on the machine and print a friendly risk summary, no flags and no license. -o writes a shareable redacted report (HTML or Markdown); --json emits the redacted scan for scripting. The scan is scoped by the same filters as check.

-o, --out file
Write a self-contained, redacted share report to this file; .md or .markdown renders Markdown, any other extension renders HTML.
--json
Emit the redacted scan as JSON (the same shape as the serve /api/scan view) for scripting, instead of the friendly summary.
--offline
Disable all network access. Assess is already egress-free; this is the belt-and-suspenders guarantee.
--hours n
Scan window in hours; 0 (the default) scans every session on the machine. The default is 0.
--severity level
Only summarize findings at or above this severity; the default (low) scans everything and curates the display. One of: low, medium, high, critical. The default is low.
--cli name
Restrict the scan to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict the scan to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--session id
Assess a single session by id.
--rules path
Load a custom TOML rules file in addition to the bundled rules.
--baseline path
Suppress findings recorded in this accepted-findings baseline. Without this flag, ~/.config/skarn/baseline.json is applied automatically when it exists. A directory of baselines requires a Team license; assess never gates, so an unlicensed directory degrades to a warning and an unsuppressed scan.
--no-color
Disable colored output.

Vet options

Statically vet this machine's AI assistant configuration - hook commands, MCP server definitions, permission grants, and installed plugins and skills across Claude Code, Codex CLI, Cursor, Copilot, and Gemini CLI - and report the risky patterns it finds: hooks that exfiltrate local content or fetch-and-execute remote code, hooks that rewrite an assistant's own config, MCP endpoints on remote hosts, MCP launchers with no pinned version or digest, permission grants that approve a whole class of actions, and extensions installed from a remote source or carrying no integrity marker. Vet is read-only and offline: it opens each config file for reading, never writes one, and makes no network call. A config file that does not exist is skipped; a config file that exists but cannot be read is a coverage gap that fails closed under a --fail-on flag. Vet surfaces risky configuration and does not change it.

--format fmt
Output format. json and sarif carry the same rule ids, severities, config paths, and locators as the text list. One of: text, json, sarif. The default is text.
--fail-on-severity level
Exit 1 if any finding is at or above this severity. Without it, vet always exits 0: it surfaces, it does not gate. One of: low, medium, high, critical.
--fail-on-scan-error
Fail closed (exit 6) when a config file that exists could not be read, so a partial view of the configuration is never reported as clean. Implied by --fail-on-severity. A home directory that cannot be resolved at all exits 6 regardless of this flag, since nothing was examined.
--offline
Disable all network access. Vet never makes a network call; this is the belt-and-suspenders guarantee.
--no-color
Disable colored output.

Baseline options

Interactive baseline triage. audit walks each new finding and records a true/false-positive decision with a reason into the committed baseline; accept records a single false-positive fingerprint non-interactively. The audit scan is scoped by the same filters as check. A false positive is suppressed on every later scan, while a finding labeled a true positive is recorded and keeps firing until the credential is rotated - the same labels the real-time guard honors. A finding that carries a secret is fingerprinted by that secret, so its acceptance follows the secret across sessions and machines; a finding with no secret (a behavioral finding such as prompt poisoning) is fingerprinted by the session it fired in, so accepting it silences that rule in that session only and the same rule in any other session keeps firing. To accept a finding the guard flagged, use `skarn guard accept <fingerprint>`: it takes the fingerprint straight from the guard's audit record and defaults to the personal baseline the guard reads.

--reason text
Reason recorded with an accepted finding (audit prompts for one; accept takes it as a flag).
--identity id
Override the accepted-by identity stamped on the decision; defaults to $SKARN_IDENTITY.
--cli name
Restrict the audit scan to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict the audit scan to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--session id
Audit a single session by id.
--rules path
Load a custom TOML rules file for the audit scan in addition to the bundled rules.
--hours n
Audit scan window in hours; 0 means no time limit. The default is 720.
--severity level
Only audit findings at or above this severity. One of: low, medium, high, critical. The default is medium.

Guard options

Control the real-time pre-execution hook used for editor/agent integration (Claude Code, Cursor, Codex CLI, and Copilot CLI). The report subverb reads the guard's own audit log instead of an event: it summarizes the flagged calls per agent (verdicts, top rules, latency, the most recent denies), states what enforcing that window would have cost (blocks per day, prompts per day), and, when the window spans at least five days with zero denies, prints the exact command that flips the wired hooks to enforce. The guard logs only flagged calls, so every count and every rate report prints is over flagged actions, never over all traffic. The accept subverb closes that loop: every audit record carries a fingerprint (the same secret-scoped key check writes to SARIF partialFingerprints), report prints it under each deny, and `skarn guard accept <fingerprint>` records it as a false positive so the identical call stops being flagged. Both subverbs are ungated: report reads nothing but the log, and accept writes only a file. On a flagged call the guard consults the personal accepted-findings baseline it shares with check (~/.config/skarn/baseline.json, the file form only - never the org-union directory, so the guard can never hit a paid gate): a finding accepted as a false positive is dropped from the verdict, while a finding a human confirmed as a REAL leak (`skarn baseline audit`, label true-positive) is never suppressed and keeps being blocked until the credential is rotated. Only the highest-severity finding of a call carries the fingerprint, so a call hiding several distinct secrets peels one accept at a time; a correlated attack chain still denies even when every secret in the call has been accepted; and a baseline that is missing, unreadable, or malformed suppresses nothing at all - a broken baseline means more blocking, never less.

--guard-mode mode
audit reports only; enforce emits a deny or ask verdict. Enforce needs a license; an unlicensed guard runs audit-only. One of: audit, enforce. The default is audit.
--strict
Also gate Read operations (credential-file reconnaissance).
--agent name
Hook host (default: auto-detect from the event shape; pass codex explicitly for Codex CLI, which shares Claude's event names, and copilot explicitly for Copilot CLI, whose Claude-compat payloads are indistinguishable from Claude's). With --self-test, all (the default) tests the claude, cursor, codex, and copilot adapters, copilot on both its CLI and VS Code dialects. With report, it restricts the summary to one agent (repeatable or comma-separated). One of: claude, cursor, codex, copilot, all.
--self-test
Verify the guard wiring instead of reading an event: feed two synthetic fixture events (one benign, one carrying a fake credential) through each adapter's real code path and report the verdicts, latency, and a guard-log write probe. The fake credential is only ever printed redacted, and the self-test deliberately ignores the accepted-findings baseline, so an accepted fingerprint can never make a dead hook look alive. Exit 0 when every adapter passes, 1 otherwise; works unlicensed (audit semantics).
--log path
With report: guard log to summarize, repeatable. Default: $SKARN_GUARD_LOG when set, otherwise the log skarn setup wires for each agent ($XDG_CACHE_HOME/skarn/guard-<agent>-audit.jsonl). A log that does not exist is named in the output with the command that wires it, never silently counted as zero.
--window span
With report: how far back to summarize, as <N>d or all. The default is 7d.
--format fmt
With report: text renders the summary, the enforce-cost forecast, and the recommendation; json emits the whole aggregate as one object with stable field names for fleet scripts (including a forecast object and the denominator string that names what its rates are rates of). One of: text, json. The default is text.
--baseline file
With accept: the accepted-findings file to write. Defaults to the personal baseline the guard itself reads (~/.config/skarn/baseline.json, honoring $XDG_CONFIG_HOME), created on first use - unlike `skarn baseline accept`, which always names its file, because an accept the guard does not read would not stop the block. Always a file, never a directory: the org-union form is not reachable from the guard. Accepting a fingerprint already recorded as a confirmed true positive is refused, so a fingerprint-only accept can never re-silence a real leak.
--reason text
With accept: why this finding is a false positive, recorded in the baseline entry. Do not paste a secret here; the text is stored verbatim.
--identity id
With accept: override the accepted-by identity stamped on the entry; defaults to $SKARN_IDENTITY.

Setup options

Guided onboarding: detect installed AI coding agents (Claude Code, Cursor, Codex CLI, Copilot CLI), merge the skarn guard hook into each one's native config in audit mode, and prove the wiring with the guard self-test. --scope project writes committed, shareable configs whose commands are portable by construction (a PATH-guarded invocation and a literal $HOME log path, never this machine's absolute paths); --plugin commits the claude plugin requirement instead of hook entries, and the two project routes are mutually exclusive - each removes the other's skarn-owned entries so the guard can never fire twice. Existing foreign hooks are never touched; every modified file is backed up first (*.skarn-bak.<timestamp>) and rewritten atomically; a target file that does not parse as JSON is refused, never overwritten. On a terminal it runs an interactive wizard with default-yes prompts; --yes accepts everything, --print writes nothing.

--agent name
Target agents, repeatable or comma-separated. Default: all detected (an agent is detected when its config directory exists or its CLI is on PATH); naming an agent explicitly also targets it when undetected. One of: claude, cursor, codex, copilot, all.
--mode mode
Guard mode written into the hook commands. Audit records would-be verdicts to the guard log without blocking; start there, review the log, then flip to enforce. Enforce needs a license to actually block; an unlicensed guard runs audit-only regardless. One of: audit, enforce. The default is audit.
--strict
Add --strict to the generated guard commands (also gates Read operations; fail-closed semantics on Codex).
--log path
Guard log path wired into the hook commands as the SKARN_GUARD_LOG prefix; pass none to disable logging. The Codex commandWindows and Copilot powershell forms carry their own Windows path (set the variable in the environment on Windows). The default is ~/.cache/skarn/guard-<agent>-audit.jsonl.
--command path
The guard executable invoked by the hook commands. At user scope the default resolves this binary's absolute path (preferring the stable Homebrew <prefix>/bin symlink over the versioned Cellar path) because GUI-launched hosts like Cursor do not inherit brew's PATH; pass skarn to opt into PATH-relative. At project scope the default is the bare name skarn, and a value containing a path separator is refused: a committed config must resolve skarn from PATH on every machine. The default is this binary's absolute path.
--scope scope
Write user-level configs (~/.claude/settings.json, ~/.cursor/hooks.json, ~/.codex/hooks.json, ~/.copilot/hooks/skarn.json) or project-level ones (./.claude, ./.cursor, ./.codex, ./.github/hooks/skarn.json relative to the working directory), meant to be committed and shared. The two scopes deliberately write different command shapes: user scope bakes this binary's absolute path (immune to a GUI host's missing PATH), project scope emits a portable PATH-guarded command (if command -v skarn ...; then ...; else exit 0; fi) with a literal $HOME log path, so the committed file runs unmodified on every teammate's machine and exits 0 where skarn is not installed. Copilot honors $COPILOT_HOME as its user-scope root. One of: user, project. The default is user.
--plugin
With --scope project, claude only: write the committed plugin requirement (the skarn entry in extraKnownMarketplaces plus enabledPlugins["skarn-guard@skarn"]) instead of hook entries, so every contributor who opens the repo installs the skarn-guard plugin from the public marketplace. Mutually exclusive with the hooks route in both directions: --plugin removes any skarn-owned hook entries it previously wrote, and a later hooks-route run removes the plugin keys, so the guard can never fire twice per event. Refused at user scope, for non-claude agents, and combined with --update, --mode, --strict, --log, or --command (the plugin ships its own hook files). Foreign marketplaces and plugins are never touched.
--print
Print the final per-agent config to stdout and write nothing; the paste-it-yourself and fleet-template path.
--format fmt
With --print: json emits one {agent, path, content} envelope per agent for machine consumption. One of: text, json. The default is text.
-y, --yes
Non-interactive: accept every prompt's default. Required when running without a terminal.
--update
Only touch agents whose config already carries skarn entries; the audit-to-enforce flip is skarn setup --update --mode enforce.
--remove
Surgically remove skarn-owned hook entries, dropping any matcher groups and event arrays they leave empty. Foreign hooks are untouched, the file itself is never deleted, and backups from earlier runs are kept.
--json
Print a machine-readable result summary (per agent: path, action, backup, self-test result) instead of the text report.

Doctor options

Health, wiring, and dead-hook detection. Doctor runs one named check per line - the binary and its path, the license, each detected agent's hook config, whether each wired hook command still resolves to an executable, whether the guard mode the hook asks for (audit or enforce) is what actually runs under this binary's license, whether the wiring matches what this version of skarn writes, the guard log's freshness, the session stores it can enumerate, the guard self-test, the cache directory, and how current the detection rules are - and every warning or failure names the exact command that clears it, or the resource that explains it when no single command applies (a free binary whose bundled rules have aged has no command to run, so that one check carries the editions link instead of a fix). Each agent's hook config is read as ordered precedence layers - the MDM/fleet-managed system paths first, then the repo's committed project config in the working directory (./.claude/settings.json, ./.cursor/hooks.json, ./.codex/hooks.json, ./.github/hooks/skarn.json), then the per-user config - so a machine wired only through the managed layer reports as protected and names the managed file, and a repo carrying a committed project config is inspected wherever doctor runs; on Claude and Codex a managed lockdown key (allowManagedHooksOnly / allow_managed_hooks_only) makes every lower layer inert, and doctor reports an inert user or project hook instead of counting it as protection (Cursor and Copilot layer additively, so every present layer runs). A committed project hook gets its own guard-command verdicts: an absolute machine-local path or an unrendered __SKARN_PATH__ placeholder FAILs (that file protects nobody but its author), a PATH-resolved command that does not resolve on this machine FAILs with the install command as the fix (the hook exits 0 here, so the repo is not protecting this machine), and Cursor at project scope WARNs naming the GUI-PATH gap - the deliberate, documented cost of a portable committed config. The license check WARNs when there is no license at all (check will not run, exit 7; assess, the guard, and the recall commands still work) and FAILs only when a license is present but broken. The eula check PASSes when an EULA acceptance is recorded (showing the accepted version, timestamp, and method) and WARNs when none is, with fix `skarn eula accept` - doctor itself never asks, so a diagnostic is never blocked by the acceptance gate. The guard-scope check WARNs when a hook wired with --guard-mode enforce runs on an unlicensed binary (the guard silently runs audit: it records the would-be verdict and blocks nothing). The guard-conformance check appears only when a managed layer declares a skarn hook and answers whether this machine runs what the org declared: it WARNs when the managed layer declares enforce on an unlicensed binary (the fleet is observing, not blocking), when the declared command does not resolve to an executable (the managed config alone deploys nothing - the binary must ride the same MDM payload), when a declared event is not gated, or when a hook disagrees with the declared mode; every branch is a WARN with an exact fix, never a FAIL, because a policy divergence is a fleet fact, not a broken install. Check ids (binary, license, eula, agent_config_<agent>, guard_command_<agent>, guard_scope_<agent>, config_drift_<agent>, guard_log_<agent>, self_test_<agent>, guard_conformance_<agent>, session_stores, cache_dir, rule_age) are a stable, greppable contract for fleet scripts. A failure exits 1; a warning never does; --json is a data mode and always exits 0. Doctor is hermetic: it makes no network call, shells out to nothing, and enumerates the session stores without reading a session's content. Set SKARN_MANAGED_ROOT to validate a staged managed payload before pushing it; the active root is echoed in the report header and in --json, so a staged report can never pass as a live one.

-v, --verbose
Add detail to every check: the paths searched, each wired hook command, the per-tool session counts, and the resolved Homebrew path.
--json
Emit the full structured result: {version, checks[{id, status, detail, fix, notes}], summary{pass, warn, fail}}, plus managed_root at the top level when SKARN_MANAGED_ROOT is active and an additive layers array ([{layer, path, effect, lockdown, wired}]) on each agent config check. Always exits 0.

Serve options

Control the localhost web UI. Access requires the per-run token embedded in the URL the command prints at startup; opening that URL sets a session cookie, and scripts may pass it as a token= query parameter instead. The recall views (search, recent, stats) need no license; the security view (/api/scan) is the same engine as check and requires one the same way - without it the endpoint answers 403 and the Security tab shows how to register free. Opening a finding in the Security view shows a detail drawer (severity, taxonomy crosswalk, attack-chain context, and the redacted code context) from which you can accept the finding into your baseline as a false positive - the same write path as skarn baseline accept, recording only the finding's fingerprint, never its secret. The license is resolved once at startup, so installing one while the server is running needs a restart.

--port n
Port to listen on; 0 asks the OS to assign one. Always binds 127.0.0.1 only. The default is 7777.
--rules path
Load a custom TOML rules file in addition to the bundled rules; the web Security scan then also surfaces its findings.
--baseline path
Suppress findings recorded in this accepted-findings baseline; the Security view hides them by default behind a show/hide toggle. Without this flag, ~/.config/skarn/baseline.json is applied automatically when it exists. A directory of baselines (the org form) requires a Team license.
--fleet path
Serve a Fleet roster view (preview) over this fleet.json aggregate of per-machine redacted scan artifacts. The aggregate is produced by an external step that reads each machine's dropped scan output; skarn neither collects nor transmits fleet data. Requires an Enterprise license.

Global options

Available on a bare invocation.

--version
Print the version and exit.
-h, --help
Print help and exit.

Recent options

List and filter recent sessions.

--cli name
Restrict to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--worktree path
Restrict to sessions whose working directory is under this path.
--since date
Lower bound of the time window; takes priority over --hours.
--until date
Upper bound of the time window.
--tz-offset minutes
Timezone offset in minutes for date grouping and display. The default is 0.
--hours n
Time window in hours; 0 means no limit. The default is 24.
--format fmt
Output format. One of: text, json, csv. The default is text.
--include-empty
Include sessions with no user or assistant messages (off by default).

Messages options

Show one session's messages. The session is a positional id or --session; the other flags only narrow which session a prefix resolves to.

--cli name
Restrict to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--worktree path
Restrict to sessions whose working directory is under this path.
--since date
Lower bound of the time window; takes priority over --hours.
--until date
Upper bound of the time window.
--tz-offset minutes
Timezone offset in minutes for date grouping and display. The default is 0.
--format fmt
Output format. One of: text, json. The default is text.
--session id
Session id to operate on (prefix-matched); equivalent to the positional argument.
-v, --verbose
Verbose output.
--hours n
Time window in hours used only to resolve a session prefix; 0 applies no filter. The default is 0.

Search options

Search past sessions. The query is positional and flag order is free.

--cli name
Restrict to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--worktree path
Restrict to sessions whose working directory is under this path.
--since date
Lower bound of the time window; takes priority over --hours.
--until date
Upper bound of the time window.
--tz-offset minutes
Timezone offset in minutes for date grouping and display. The default is 0.
--session id
Restrict to a single session by id (prefix-matched).
--hours n
Time window in hours; 0 searches all history (the default). The default is 0.
--all
Search all history (explicit alias for the default).
--context n
Lines of context to show around each match. The default is 2.
--role role
Restrict matches to one message role. One of: user, assistant, all. The default is all.
--source src
Restrict matches by content source kind. One of: content, tool-input, tool-result, all. The default is content.
--max-results n
Cap how many match rows are rendered; 0 means no cap. The default is 50.
--max-line-length n
Cap rendered snippet length per row; 0 means no cap. The default is 200.
--list
Print one matching session per line instead of per-match rows.
--count
Print one matching session per line with its hit count.
--output fmt
Output format for the active mode. One of: text, json. The default is text.
--json
Shorthand for --output json.
--regex
Treat the query as a PCRE2 regular expression.
--case-sensitive
Case-sensitive matching (default: case-insensitive).

Restore options

Restore a session by id.

--session id
Session id to operate on (prefix-matched); equivalent to the positional argument.

Stats options

Session analytics. A positional session id switches to a per-session view.

--cli name
Restrict to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--worktree path
Restrict to sessions whose working directory is under this path.
--since date
Lower bound of the time window; takes priority over --hours.
--until date
Upper bound of the time window.
--tz-offset minutes
Timezone offset in minutes for date grouping and display. The default is 0.
--hours n
Time window in hours; 0 means no limit. The default is 168.
--session id
Restrict to a single session by id (prefix-matched).
--format fmt
Output format. csv requires --by; html writes a file (-o). One of: text, json, csv, html. The default is text.
--by axis
Group the aggregate by date or project; required for --format csv. One of: date, project.
--show list
Comma list of content blocks to render (default: overview,tokens). text and html only. One of: overview, tokens, models, tools, timing, all.
--open
Open the HTML report in a browser after writing it (--format html only).
-o, --out file
Destination file for --format html. The default is skarn-report.html.

Export options

Export session data.

--cli name
Restrict to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--worktree path
Restrict to sessions whose working directory is under this path.
--since date
Lower bound of the time window; takes priority over --hours.
--until date
Upper bound of the time window.
--tz-offset minutes
Timezone offset in minutes for date grouping and display. The default is 0.
--hours n
Time window in hours; 0 means no limit. The default is 168.
--session id
Restrict to a single session by id (prefix-matched).
--format fmt
Output format. One of: text, json, ndjson, html. The default is text.
--no-redact
Emit raw session content; by default every detected secret is redacted.
--out file
Write to this file instead of stdout.
--open
Open the written file in the default viewer (needs --out).
--gzip
Gzip-compress the output (needs --out, or --format html).

Cmds options

Shell commands run across sessions.

--cli name
Restrict to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--worktree path
Restrict to sessions whose working directory is under this path.
--since date
Lower bound of the time window; takes priority over --hours.
--until date
Upper bound of the time window.
--tz-offset minutes
Timezone offset in minutes for date grouping and display. The default is 0.
--hours n
Time window in hours; 0 means no limit. The default is 168.
--format fmt
Output format. One of: text, json. The default is text.
--session id
Restrict to a single session by id (prefix-matched).
-v, --verbose
Verbose output.
--failed
Only show entries that failed (errored or exited non-zero).
--unique
Collapse duplicates, keeping the latest occurrence.
--cmd text
Case-insensitive substring filter on the command text.

Mcps options

MCP calls made across sessions.

--cli name
Restrict to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--worktree path
Restrict to sessions whose working directory is under this path.
--since date
Lower bound of the time window; takes priority over --hours.
--until date
Upper bound of the time window.
--tz-offset minutes
Timezone offset in minutes for date grouping and display. The default is 0.
--hours n
Time window in hours; 0 means no limit. The default is 168.
--format fmt
Output format. One of: text, json. The default is text.
--session id
Restrict to a single session by id (prefix-matched).
-v, --verbose
Verbose output.
--failed
Only show entries that failed (errored or exited non-zero).
--unique
Collapse duplicates, keeping the latest occurrence.
--mcp name
Substring filter on the MCP tool name.

Tools options

Tools used across sessions.

--cli name
Restrict to one assistant's sessions. One of: claude, gemini, codex, cursor, copilot.
--project path
Restrict to matching projects: a comma-separated list of names or paths, each matched case-insensitively against a session's project display name, its canonical root path, or a parent directory of that root.
--worktree path
Restrict to sessions whose working directory is under this path.
--since date
Lower bound of the time window; takes priority over --hours.
--until date
Upper bound of the time window.
--tz-offset minutes
Timezone offset in minutes for date grouping and display. The default is 0.
--hours n
Time window in hours; 0 means no limit. The default is 168.
--format fmt
Output format. One of: text, json. The default is text.
--session id
Restrict to a single session by id (prefix-matched).
-v, --verbose
Verbose output.
--failed
Only show entries that failed (errored or exited non-zero).
--unique
Collapse duplicates, keeping the latest occurrence.
--tool name
Substring filter on the tool name.

License options

Show the active license, install one, or renew it. With no argument it reports what license this machine is using, where it came from, and when it expires. With a file argument (or - to read stdin) it validates the license and installs it to the config directory, backing up any license already there. With renew it asks the license service for a fresh license for the current subscription, verifies the reply against the signing keys built into this binary BEFORE writing a single byte, and installs it; a reply that does not verify is discarded and nothing is written. Verification is not signature-only: the reply must carry the same license id and move the expiry forward, so a valid artifact belonging to another subscription is refused too. Renewal happens only when you type it - check, the recall verbs, serve, and guard never renew on their own - and it is the only network call skarn makes besides the opt-in maintained-feed fetch. The endpoint defaults to https://api.getskarn.com and is overridable with $SKARN_LICENSE_RENEW_URL; it must be https (plain http is accepted only against loopback), so a poisoned environment cannot send the license anywhere in cleartext. Renew exits 0 when the new license is installed, 1 when the service cannot be reached, 2 when the service declines (its reason is printed verbatim), and 3 when the reply fails verification. It never prints the license token itself, and it works without a license - and with an expired one - so it never exits 5.

-o, --output path
Write the renewed license to this path instead of the config directory. Only applies to `skarn license renew`; the file is written with owner-only permissions, and an existing file that is not a readable Skarn license is never overwritten.
-y, --yes
Replace an already-installed license without asking. Required when installing over an existing license from a script or from stdin, where there is no terminal to confirm on.
--json
Print the license status as a single JSON object (state, tier, licensee, org, seats, license id, issue and expiry dates, days remaining, and the source it was resolved from). The license token is never included.

Exit status

0
Clean, or informational output only.
1
A finding at or above the --fail-on-severity threshold was reported. For setup and guard --self-test: a target was refused or a verification failed. For doctor: at least one check FAILED (a warning never exits nonzero, and --json always exits 0). Also: the first-run EULA prompt was declined, so the invoked operation was not performed.
2
The session risk score exceeded the --fail-on-risk threshold. For setup: no terminal to prompt on and no --yes; the exact non-interactive command is printed.
3
A canary token was triggered (a proven breach). This overrides every other gate.
4
A policy precondition was not met: a required rule is missing, or the policy requires a baseline and none was supplied.
5
A requested paid feature is not covered by the active license (a Team or Enterprise flag without a matching license). Refused before any scan. With no usable license at all, check exits 7 before this gate is consulted.
6
The scan could not complete: session discovery or the scan itself failed. For vet: the home directory could not be resolved (so the configuration was never located at all), or a config file that exists could not be read, so the configuration was only partly seen. Fail-closed - the result is not trustworthy, so it is never reported as clean.
7
No usable license is present - none was found via SKARN_LICENSE, SKARN_LICENSE_FILE, or the installed file - so check refused before scanning. The free license is issued at https://getskarn.com/free after a quick email confirmation and is verified offline. Also used when a license IS present but cannot be used at all (revoked, tampered, or signed by an untrusted key). assess, the recall commands, guard, setup, doctor, vet, license, taxonomies and check --audit-verify never exit 7.

Environment

SKARN_SCAN_THREADS
Cap the worker-pool size for every parallel sweep (scan and recall). 1 forces a fully serial run. Default is one worker per core, capped at 32. Output is identical for any value; only latency changes.
SKARN_CLAUDE_DIRS
Colon-separated list of additional Claude Code config directories to scan beyond the default ~/.claude. Each entry is the dir that contains projects/. Roots are de-duplicated.
SKARN_TZ_OFFSET
Default timezone offset in hours applied when grouping recall and stats output by time.
SKARN_NO_TIMING
Suppress the dim "Scanned N sessions" timing line that recall commands print after a text run on a terminal. Already auto-suppressed for json/sarif/csv output and for non-TTY stdout.
SKARN_LICENSE_RENEW_URL
License-service endpoint `skarn license renew` posts to. Defaults to https://api.getskarn.com/v1/license/renew; set it to route renewals through an enterprise proxy. It must be https - plain http is accepted only against loopback - so a poisoned environment cannot redirect the license token to a cleartext endpoint. No other command reads it.
SKARN_GUARD_LOG
Path to append a redacted JSONL verdict log for guard (opt-in; off when unset). One record per flagged call, carrying the fingerprint that `skarn guard accept` takes; clean calls, and calls whose every finding was already accepted, are not logged.
SKARN_IDENTITY
Local-first identity tag (an SSO subject, email, or CI principal) stamped as provenance on baseline entries created with --baseline-create. Optional; Skarn never authenticates it.
SKARN_ORG
Local-first org or team tag stamped alongside SKARN_IDENTITY on baseline provenance. Optional.
SKARN_FEED_URL
Default feed channel URL used by check --update-rules when --feed-url is not given (Team).
SKARN_FEED_TOKEN
Credential presented to the subscriber feed channel when fetching a maintained feed (Team).
SKARN_EULA_ACCEPTED
Set to 1 to suppress the first-run EULA prompt and notice for this run without recording anything (ephemeral environments: containers, CI). Running Skarn constitutes acceptance either way; `skarn eula accept` records it durably instead.
SKARN_LICENSE
License token, resolved before SKARN_LICENSE_FILE and the config path.
SKARN_LICENSE_FILE
Path to a file containing a license token.
CLAUDE_CONFIG_DIR
A Claude Code config directory visible in Skarn's environment; folded into the claude source like an SKARN_CLAUDE_DIRS entry.
COPILOT_HOME
GitHub Copilot CLI config directory visible in Skarn's environment; replaces the default ~/.copilot as the Copilot CLI session root, folded into the copilot source.
SKARN_MANAGED_ROOT
Prefix doctor prepends to every managed (MDM/fleet) config path it reads, so a fleet admin can validate a staged payload directory before pushing it to a single machine (SKARN_MANAGED_ROOT=/tmp/stage skarn doctor). Empty or unset means the live system paths. Whenever it is active, doctor echoes the root in the report header and emits it as managed_root in --json, so a staged report can never pass as a report about the live machine. Only doctor reads it.
XDG_CACHE_HOME
Base directory for the rule and feed cache. Defaults to ~/.cache, so the cache lives in ~/.cache/skarn/.

Files

/Library/Application Support/ClaudeCode/managed-settings.json, /etc/claude-code/managed-settings.json, C:\Program Files\ClaudeCode\managed-settings.json
Claude Code's MDM-managed settings (macOS, Linux/WSL, Windows). Doctor reads the platform's path as the claude host's managed precedence layer; allowManagedHooksOnly: true there makes every lower layer inert. Read-only; never written.
/Library/Managed Preferences/com.anthropic.claudecode.plist
Claude Code's macOS managed-preferences domain. Declared as a managed source but not decoded; when present, doctor surfaces it as a coverage gap with a plutil command to inspect it, never as a silent absent.
/etc/codex/requirements.toml, %ProgramData%\OpenAI\Codex\requirements.toml
Codex's admin-managed requirements file (Unix, Windows). Doctor reads it as the codex host's managed precedence layer via a scoped reader (allow_managed_hooks_only and the [[hooks.<Event>.hooks]] command strings); allow_managed_hooks_only = true makes every lower layer inert. Read-only; never written.
/Library/Managed Preferences/com.openai.codex.plist
Codex's macOS managed-preferences domain. Doctor decodes the XML form (requirements_toml_base64 and config_toml_base64, base64 to the same scoped reader; the lockdown key is honored only from the requirements payload, matching Codex); a binary plist is surfaced as present-but-unreadable with the exact plutil conversion command, never as a silent absent.
/Library/Application Support/Cursor/hooks.json, /etc/cursor/hooks.json, C:\ProgramData\Cursor\hooks.json
Cursor's enterprise-managed hooks (macOS, Linux/WSL, Windows). Doctor reads the platform's path as the cursor host's managed precedence layer; Cursor layers additively (no lockdown key), so every present layer stays active. Read-only; never written.
/etc/github-copilot/policy.d/*.json, C:\ProgramData\GitHub\Copilot\policy.d\*.json
Copilot's managed policy directory (Linux/macOS, Windows). Doctor folds every *.json in the directory into the copilot host's managed precedence layer, sorted by name; Copilot layers additively. Read-only; never written.
./.claude/settings.local.json
Claude Code's personal, gitignored project settings file. Claude merges hooks across every settings file and runs all of them, so a guard wired here protects this checkout and doctor reads it as part of the project layer. skarn setup never writes it: it writes only the committed ./.claude/settings.json.
./.claude/settings.json, ./.cursor/hooks.json, ./.codex/hooks.json, ./.github/hooks/skarn.json
A repo's committed project-scope hook configs (claude, cursor, codex, copilot), written by skarn setup --scope project and read by doctor as each host's project precedence layer whenever it runs in that directory. Portable by construction: the committed command resolves skarn from PATH at run time and exits 0 where skarn is not installed.
~/.skarn.json
Optional tool configuration: the set of assistant tools and their session paths, plus an optional projects alias map. Both the recall surface and scan discovery (check, assess, baseline audit, the guard's session-end advisory, and serve's Security scan) read it, so the security scan and the recall views cover the same corpus. Falls back to a builtin tool set when absent. The projects object renames a resolved project for display: a key beginning ~, $, or / is a path prefix (with ~ and a leading $VAR expanded like sessions_path) matched against the canonical project root on whole path components, longest match winning (so /repo never matches /repository); any other key matches the derived display name case-insensitively; the value is the shown name. Aliases change display only, never the grouping key.
~/.cache/skarn/
Cache for downloaded rule and maintained-feed bundles. Honors $XDG_CACHE_HOME.
~/.config/skarn/baseline.json
The personal accepted-findings baseline, applied automatically when it exists: check, assess, and serve suppress its findings, and guard stops flagging them. Written by `skarn baseline audit/accept` and `skarn guard accept`. Honors $XDG_CONFIG_HOME. Holds fingerprints and hashes, never a raw secret; treat it as sensitive.
~/.config/skarn/eula-accepted.json
The EULA acceptance record: the agreement version accepted, an RFC-3339 timestamp, and the method (prompt, command, env, license-install). Written when acceptance is given at the first-run prompt, by `skarn eula accept`, or by installing a License Token; never transmitted. Honors $XDG_CONFIG_HOME.
~/.claude/
Default Claude Code session root. Scanned automatically; extend with $SKARN_CLAUDE_DIRS or $CLAUDE_CONFIG_DIR.
~/.claude/settings.json, ~/.claude/settings.local.json
Claude Code hooks and permission grants. Read (never written) by vet, and by the project-local ./.claude/settings.json and ./.claude/settings.local.json when one exists.
~/.claude.json, ~/.mcp.json
Claude Code MCP server definitions. Read by vet, along with a project-local ./.mcp.json when one exists.
~/.claude/plugins/, ~/.claude/skills/
Installed Claude Code plugins and skills. Vet reads each plugin.json and skill.json manifest for its install source and integrity marker.
~/.codex/config.toml, ~/.codex/hooks.json
Codex CLI MCP server definitions and hooks. Read by vet.
~/.cursor/mcp.json, ~/.cursor/hooks.json
Cursor MCP server definitions and hooks. Read by vet.
~/.copilot/mcp-config.json, ~/.copilot/config.json
Copilot CLI MCP server definitions and configuration. Read by vet.
~/.gemini/settings.json
Gemini CLI MCP server definitions. Read by vet.

Guard hook hosts

The hook events skarn guard receives on each supported host, what each event lets the guard scan, and the decision channel it answers on. Events marked opt-in are supported but not wired by the shipped configs.

Claude Code

Claude Code fires PascalCase events with a JSON envelope on stdin. Five events are wired by default; the two output-scanning events are supported but opt-in because they fire on every tool result.

Codex shares Claude's PreToolUse and PermissionRequest names, so auto-detection keys on a Codex-only tell (turn_id, or the apply_patch/spawn_agent tools); a Claude event carries none of them and routes to the Claude adapter. VS Code Copilot also shares the envelope, so its own tool-name vocabulary routes it to the Copilot adapter. The shipped Claude PermissionRequest hooks pass --agent claude explicitly anyway.

A non-zero guard exit fails closed on the blocking events. An unparseable event asks in enforce mode.

PreToolUse
Scans tool_name plus the flattened tool_input; a Bash command is also carried raw so a tool description cannot poison the package parse. Enforce: hookSpecificOutput.permissionDecision = deny or ask, with the redacted reason in permissionDecisionReason. Audit: hookSpecificOutput.additionalContext. Wired by the shipped configs. A deny blocks the call even under bypassPermissions: hooks run before permission-mode checks.
PermissionRequest
Scans the same tool_name plus tool_input as PreToolUse, at the permission-decision step. Enforce: hookSpecificOutput.decision.behavior = deny, with the redacted reason on the universal systemMessage. Audit: systemMessage. Wired by the shipped configs. A second chokepoint for a call PreToolUse deferred or allowed. The decision has only allow/deny and no reason field, so a would-be ask degrades to deny and additionalContext is invalid here.
UserPromptSubmit
Scans prompt. The channel ships content off the machine, so a secret on it is exfiltration. Enforce: top-level decision = block, with the redacted reason in reason. Audit: hookSpecificOutput.additionalContext. Wired by the shipped configs. A lone secret is enough to deny here - the destination is not ambiguous. The block is recoverable: the user edits and resubmits.
UserPromptExpansion
Scans command_name and expanded_prompt. The channel ships content off the machine, so a secret on it is exfiltration. Enforce: top-level decision = block, with the redacted reason in reason. Audit: hookSpecificOutput.additionalContext. Wired by the shipped configs. The post-expansion text of a slash or custom command - content UserPromptSubmit never sees - gated before it reaches the model.
TaskCreated
Scans task_title and task_description. The channel ships content off the machine, so a secret on it is exfiltration. Enforce: continue = false, with the redacted reason in stopReason (rolls the task creation back). Audit: systemMessage. Wired by the shipped configs. A task is subagent fan-out, so it is modeled as the canonical spawn_agent tool. The event has no decision channel and does not support additionalContext.
PostToolUse (opt-in)
Scans tool_output (falling back to tool_response) of an in-scope tool. Enforce: top-level decision = block, with the redacted reason in reason. Audit: hookSpecificOutput.additionalContext. Supported but opt-in: not wired by the shipped configs. Output scanning: catches a secret in a tool RESULT before the model sees it. Opt-in - it fires on every tool result. Prefer PostToolBatch, which sees the same content once per batch.
PostToolBatch (opt-in)
Scans tool_output and tool_error of every in-scope call in tool_calls[]. Enforce: top-level decision = block, with the redacted reason in reason. Audit: hookSpecificOutput.additionalContext. Supported but opt-in: not wired by the shipped configs. The batch analog of PostToolUse: one invocation per resolved parallel batch, so the cheaper of the two output gates. Opt-in. The batch is scanned as one synthetic interaction named after its first in-scope call.
Stop
Scans nothing from the event: it triggers a throttled, scoped, offline scan of recent sessions on this machine. Enforce: never blocks (advisory only). Audit: systemMessage with suppressOutput. Wired by the shipped configs. The session-end advisory. At most once an hour it reports a redacted count and the highest severity of credential exposures found in recent sessions, and points at skarn assess. Never egresses, never prints a secret value.

Cursor

Cursor fires camelCase events, each with its own stdin shape and its own verdict schema - unlike Claude's single envelope. All four blocking events are wired by default.

The camelCase event names are unique to Cursor, so auto-detection is unambiguous.

Fail-open by default; --strict plus enforce exits 2 (Cursor's schema-agnostic hard block) on an unparseable event.

beforeShellExecution
Scans command (mapped to the canonical Bash tool). Enforce: permission = deny or ask, with the redacted reason in user_message and agent_message. Audit: none - Cursor's blocking events carry no non-deciding channel, so audit stays silent and relies on the guard log. Wired by the shipped configs.
beforeReadFile
Scans file_path as the input and content as the result (credential-file recon and dotenv contents). Enforce: permission = deny (allow/deny only, so a would-be ask degrades to deny). Audit: none (see beforeShellExecution). Wired by the shipped configs. Gated only under --strict, exactly like Claude's Read; the shipped Cursor config passes --strict on this event alone.
beforeMCPExecution
Scans tool_input, under the canonical mcp__<server>__<tool> name. Enforce: permission = deny or ask, with the redacted reason in user_message and agent_message. Audit: none (see beforeShellExecution). Wired by the shipped configs.
beforeSubmitPrompt
Scans prompt. The channel ships content off the machine, so a secret on it is exfiltration. Enforce: continue = false, with the redacted reason in user_message. Audit: none (see beforeShellExecution). Wired by the shipped configs. No permission channel, so a would-be ask blocks too. The block is recoverable: the user edits and resubmits.

Codex CLI

Codex's hook system mirrors Claude's - PascalCase events and a near-identical PreToolUse JSON - but its verdict semantics differ: permissionDecision ask and a bare allow both FAIL OPEN, so a would-be ask degrades to deny.

Codex shares Claude's event names, so pass --agent codex explicitly. Auto-detection falls back to a Codex-only tell: turn_id (on every turn-scoped event, PermissionRequest included) or the apply_patch/spawn_agent tools.

Fail-open by default; --strict plus enforce emits an explicit deny JSON on an unparseable event.

PreToolUse
Scans tool_name plus the flattened tool_input; only Bash carries a raw shell command (apply_patch's command field is patch text). Enforce: hookSpecificOutput.permissionDecision = deny, with the redacted reason in permissionDecisionReason. Audit: continue = true plus systemMessage (Codex PreToolUse has no additionalContext). Wired by the shipped configs.
PermissionRequest
Scans the same tool_name plus tool_input as PreToolUse. Enforce: hookSpecificOutput.decision.behavior = deny, with the redacted reason in message. Audit: systemMessage. Wired by the shipped configs. The approval-flow gate: any deny wins, closing PreToolUse's updatedInput and ask fail-open edges.
UserPromptSubmit
Scans prompt. The channel ships content off the machine, so a secret on it is exfiltration. Enforce: top-level decision = block, with the redacted reason in reason. Audit: systemMessage. Wired by the shipped configs.
PostToolUse (opt-in)
Scans tool_response of an in-scope tool. Enforce: top-level decision = block, with the redacted reason in reason. Audit: systemMessage. Supported but opt-in: not wired by the shipped configs. Output scanning. Opt-in - it fires on every tool result.

GitHub Copilot CLI

The Copilot CLI's native payload is camelCase (toolName / toolArgs / sessionId) and carries no event-name field, so the event is inferred from the field shape. Only preToolUse can actually block; the other events run full detection and write the audit log but cannot decide.

The camelCase field set is the CLI tell. A Copilot payload in Claude-compat mode is byte-indistinguishable from a real Claude event (field names AND tool names), so the shipped Copilot configs always pass --agent copilot explicitly.

preToolUse fails CLOSED on a non-zero exit, but a hook TIMEOUT fails OPEN (the shipped configs set timeoutSec 10).

preToolUse
Scans toolName plus toolArgs (an object, or a JSON string that is parsed and flattened). Enforce: top-level permissionDecision = deny or ask, with the redacted reason in permissionDecisionReason. Audit: additionalContext. Wired by the shipped configs. The only enforcing gate on this host, and the only one where ask is a real interactive prompt rather than a degraded deny.
permissionRequest
Scans the same shape as preToolUse. Enforce: nothing is emitted - detection and the audit log only. Audit: none. Wired by the shipped configs. Byte-identical to preToolUse with no discriminator, so it cannot independently block. It is wired for detection coverage and the audit log.
userPromptSubmitted
Scans prompt. The channel ships content off the machine, so a secret on it is exfiltration. Enforce: nothing is emitted - the CLI ignores this event's hook output. Audit: none. Wired by the shipped configs. A secret typed into a Copilot CLI prompt is detected and logged but NOT blocked before it reaches the provider - unlike Claude Code and Codex, whose prompt-submit hooks can block. A host limitation, not a Skarn one.
preMcpToolCall (opt-in)
Scans arguments, under the canonical mcp__<server>__<tool> name. Enforce: nothing is emitted - this event's output controls only MCP request metadata. Audit: none. Supported but opt-in: not wired by the shipped configs. Supported but not wired in the shipped configs: it cannot block or advise, so it buys detection coverage in the guard log only.
postToolUse (opt-in)
Scans toolResult. Enforce: advisory only - never blocks. Audit: additionalContext. Supported but opt-in: not wired by the shipped configs. Output scanning. Opt-in - it fires on every tool result.

GitHub Copilot (VS Code agent mode)

VS Code agent mode speaks a second Copilot dialect: PascalCase event names in a Claude-shaped envelope, camelCase tool_input keys, and VS Code's own tool-name vocabulary (run_in_terminal, create_file, ...), which Skarn maps to canonical names. MCP calls arrive as PreToolUse with an mcp_<server>_<tool> name.

VS Code discovers hooks from ~/.claude/settings.json (chat.hookFilesLocations), so a machine already running Skarn's Claude guard receives VS Code Copilot events. The VS Code tool-name vocabulary routes them to the Copilot adapter instead of a silent Claude fast-allow; the shipped configs pin --agent copilot.

VS Code blocks only on exit 2; any other non-zero exit fails OPEN.

PreToolUse
Scans tool_name (VS Code vocabulary, mapped to canonical) plus the flattened tool_input. Enforce: hookSpecificOutput.permissionDecision = deny or ask - both fully honored (the exact inverse of the CLI, where the decision is top-level). Audit: systemMessage. Wired by the shipped configs.
UserPromptSubmit
Scans prompt. The channel ships content off the machine, so a secret on it is exfiltration. Enforce: top-level decision = block, with the redacted reason in reason. Audit: systemMessage. Wired by the shipped configs. Unlike the Copilot CLI, the VS Code host honors a prompt-submit block. On the cloud agent an ask degrades to deny (non-interactive).
PostToolUse (opt-in)
Scans tool_response. Enforce: top-level decision = block, with the redacted reason in reason. Audit: systemMessage. Supported but opt-in: not wired by the shipped configs. Output scanning. Opt-in - it fires on every tool result.

Examples

Show which license this machine is using, where it came from, and when it expires.

skarn license

Validate a license file and install it; the previous license, if any, is backed up alongside it.

skarn license ~/Downloads/team.skarnlicense

Fetch a fresh license for this subscription, verify it against the keys built into this binary, and install it; nothing is written unless it verifies.

skarn license renew

Scan every AI coding session on this machine and print a friendly risk summary in one command; no flags, no license.

skarn assess

Write a self-contained, redacted security report to share with a colleague; -o report.md writes a plain-text version.

skarn assess -o report.html

Statically vet this machine's AI assistant configuration - hooks, MCP servers, permission grants, plugins and skills - and list the risky patterns found. Read-only, offline, no license.

skarn vet

Emit the config findings as SARIF for a code-scanning pipeline and exit 1 on anything high or above; an unreadable config file exits 6 rather than reporting a partial view as clean.

skarn vet --format sarif --fail-on-severity high

Detect installed AI coding agents and wire the guard hook into each one in audit mode, with a wizard on a terminal; ends with the guard self-test and a list of every file touched.

skarn setup

Print the exact Cursor hooks.json content that setup would write, without writing anything.

skarn setup --print --agent cursor

Check that skarn is really protecting this machine: license, every wired agent hook, the guard log, and the guard self-test. Each warning or failure names the command that fixes it.

skarn doctor

The same checks as one JSON object for fleet scripts; the check ids are stable and always exits 0.

skarn doctor --json

Flip every agent that already carries skarn entries from audit to enforce, leaving everything else alone.

skarn setup --update --mode enforce

Summarize the last 30 days of flagged calls per agent (verdicts, top rules, latency, the most recent denies) and, when the window is clean, print the exact command that flips the hooks to enforce.

skarn guard report --window 30d

The same aggregate as one JSON object for fleet scripts: counts are over flagged actions, since the guard does not log clean calls.

skarn guard report --format json

Accept a finding the guard flagged (the fingerprint printed under the deny in guard report) as a false positive, so the identical call stops being blocked. Writes the personal baseline the guard reads; a real leak is never accepted this way.

skarn guard accept 7d343e3a105ae026589b3371d565a4ad2943dd5ba5f81a19c7500b2ab4dbb1f0 --reason 'internal test credential'

Prove the guard wiring: run synthetic benign and fake-credential events through each adapter's real code path and report the verdicts (credential shown redacted).

skarn guard --self-test

Scan the default window (the last 30 days) of all sessions for leaked secrets and attack patterns.

skarn check

Scan all history, report only high-severity-and-above findings, and emit JSON for a pipeline.

skarn check --hours 0 --severity high --format json

Exit non-zero in CI when a session's risk score exceeds 60.

skarn check --fail-on-risk 60

Search past sessions for a regular-expression pattern.

skarn search 'AWS_SECRET' --regex

List recent Claude Code sessions.

skarn recent --cli claude

Render per-project analytics to a self-contained HTML file.

skarn stats --by project --format html -o stats.html

Browse and scan sessions in a local web UI on 127.0.0.1:7777 (localhost only, no egress); open the token-carrying URL printed at startup.

skarn serve --port 7777

Serve the web UI with a custom rule pack layered on the bundled rules; the Security view surfaces its findings too.

skarn serve --rules ./team-rules.toml

Walk each new finding and label it a true or false positive with a reason; the decisions are written into the committed baseline and survive re-scans.

skarn baseline audit .skarn-baseline.json

Record a single false-positive fingerprint (from SARIF partialFingerprints or the ndjson skarn.fingerprint field) into the baseline without the interactive loop.

skarn baseline accept .skarn-baseline.json 7d343e3a105ae026589b3371d565a4ad2943dd5ba5f81a19c7500b2ab4dbb1f0 --reason 'internal test credential'

Security

Skarn scans locally and makes no network connection by default. Secrets are redacted in every output format unless --no-redact is given.

The only feature that transmits anything off the machine is the maintained feed: --update-rules and --feed-url fetch a signed feed bundle from the subscriber channel. No secret ever leaves the machine. It is off by default and is disabled by --offline.

skarn vet reads the AI assistant configuration files listed under FILES read-only: it opens each one for reading, never writes, rewrites, or removes one, and makes no network call. It reports what it finds and leaves every decision about the configuration to you. --offline is accepted for parity and changes nothing.