Repository navigation
user security audit
/security-audit is Steiner's built-in static, read-only security review workflow. It maps the attack surface of a git diff or a repository, runs per-domain review finders, adversarially verifies every candidate with a fresh evaluator, and writes one report under .steiner/security/.
It is not a scanner. It does not build, test, install, fetch the network, execute exploits, run external tools, or consult CVE and advisory data. It reads source and reasons about reachability, so findings are static hypotheses, not proof.
Use it before shipping a change or when onboarding to unfamiliar code:
- audit what a branch changed, against a resolved base
- audit the repository, or a subtree, as a whole
- get an adversarial second opinion on each candidate before you trust it
The only file the audit writes is the report. It never modifies code, configuration, or .gitignore, and never stages, commits, or reverts anything.
/security-audit diff [base-ref]
/security-audit repo [path]
-
diffaudits what changed against a resolved base. -
repoaudits the repository root, or the subtree atpath.
With no mode, an unrecognised mode, or an ambiguous argument, Steiner asks which scope to audit and waits before doing any work.
The base is the base-ref you give, otherwise the first ref that resolves, tried in this order: refs/remotes/origin/HEAD, origin/main, origin/master, main, master. If none resolves, the audit stops and asks for a base.
The changed set is the union of committed changes against the merge base, staged changes, unstaged changes, and non-ignored untracked files, with status, rename, and delete information kept. An empty set is an error; the audit never falls back to repo mode.
Repo mode audits the repository root or the given subtree, ranking files reachable from the mapped entry points ahead of the rest. The path must exist and resolve inside the repository root.
Exclusions are always listed with a reason; none are dropped silently.
- Diff mode excludes binary paths, files over 1 MiB, submodules, and anything under
.steiner/. - Repo mode excludes
vendor/,node_modules/,dist/,build/, generated files (aDO NOT EDITheader,*.pb.go,*_generated.*), and.steiner/including earlier reports. - Lockfiles are read only by the supply-chain lens.
Diff mode expands each changed symbol by one hop: direct callers and callees, the nearest entry point, and any middleware or config it names. LSP is used where available, otherwise grep.
-
Delegation must be enabled (
sub_agent.enabled: true). The audit needs thesub_agenttool to map, find, and verify; there is no inline fallback, so without it Steiner stops and tells you to enable delegation. -
.steiner/security/must be writable. If it cannot be created, the audit stops before starting. - The session's current execution mode is preserved, never switched. Plan mode already permits writes to
.steiner/security/, so the report can be written there without leaving plan mode.
The audit runs as a fixed multi-agent pipeline:
-
Map — one
exploresub-agent builds an attack-surface map: entry points, trust boundaries, sensitive sinks, authn/authz chokepoints, validation patterns, secret handling, config and deployment files, dependency manifests, and AI/LLM usage signals. It makes no vulnerability judgements. - Lens selection — lenses are selected from the map. A lens runs only when the map shows relevant surface; every skipped lens is recorded with its reason.
-
Advisor checkpoint 1 — optional, only when the
advisortool is available. Asks whether the map and chosen lenses miss an attack surface or a relevant lens. -
Find — one
reviewsub-agent per selected lens, run in parallel. -
Synthesise — parent work. Deduplicates by root cause, flags locations raised by more than one lens, promotes out-of-lens observations, and assigns stable IDs (
SA-001,SA-002, …). -
Verify — one fresh
evaluatesub-agent per candidate, given the claim as a neutral hypothesis and never the finder's reasoning, confidence, or severity. Each verifier tries to disprove the claim. -
Advisor checkpoint 2 — optional, only when the
advisortool is available. Asks whether the verdicts and severities hold up. Feedback may trigger a fresh verifier run or a severity change, but never promotes a candidate tosupportedon its own. - Report — written to disk as described below.
| Lens | Checks |
|---|---|
| Input handling & injection | String-built queries, commands, and templates; path joins without containment; unsafe deserialisation; unrestricted uploads; eval-style execution of untrusted input. |
| AuthN, AuthZ & session | State-changing handlers missing an authorization check; sibling endpoints that do check; unverified object ownership; missing CSRF; session issuance, invalidation, default or bypassable credentials. |
| Cryptography & secrets | Hardcoded credentials or keys; weak or fast password KDFs; ECB mode, static IVs, non-CSPRNG randomness; disabled TLS verification; key material that can leak. |
| Data exposure & privacy | Secrets or PII in logs; stack traces or internals returned to clients; debug features on by default; serialising more fields than the caller needs. |
| Supply chain & dependencies | Unpinned version ranges; missing lockfile; scripts that fetch and execute; deps fetched over plain HTTP; vendored code with no provenance. Hygiene only: no CVE or advisory matching. |
| Configuration & deployment | Containers running as root; secrets baked into images; CI triggers such as pull_request_target with checkout of untrusted code; actions unpinned to a SHA; overbroad tokens; IaC exposing public or wildcard access; permissive CORS or debug endpoints. |
| Business logic & resource abuse | Bypassable or repeatable workflows; missing quotas or rate limits; unbounded input driving allocation; replay or missing idempotency on state-changing operations. |
| AI/LLM-specific (conditional) | Untrusted content entering prompts; model output passed to a shell, SQL, or HTML sink; tools or agents granted broader reach than needed. |
Every candidate carries a verdict from a fixed vocabulary:
| Verdict | Meaning |
|---|---|
supported |
An independent static attempt to disprove the claim failed. Attacker control, reachability, and impact are plausible and no inspected defence blocks the path. |
refuted |
Concrete evidence defeats attacker control, reachability, the absence of a defence, or the impact. Refuted candidates go to the report's rejected list, not to findings. |
unresolved |
Static source cannot decide, for example because of runtime configuration, deployment, or unreviewed code. Unresolved candidates get their own report section, never Findings. |
supported means the attempt to disprove failed; it is not proof. Steiner never calls code "secure" or "safe", and never labels a verdict "confirmed".
Severity is the impact if the issue is real:
| Severity | Meaning |
|---|---|
Critical |
Remote code execution, full authentication bypass, or mass exposure of data or credentials. |
High |
Scoped privilege gain or data exposure, or injection with limited reach. |
Medium |
Narrow logic flaw, weak cryptography, or a missing defence-in-depth control. |
Low |
Hardening gap or minor information disclosure. |
The finder's confidence (high/medium/low) is informational only and does not affect the verdict. There is no CVSS scoring.
The report is written to .steiner/security/YYYY-MM-DD_HHMM_<diff|repo>.md in local time. It is create-only: if the name is taken, _2, _3, … are appended until free, and an existing report is never overwritten. No other file is written.
A report contains:
- title and one-line scope
- scope and methodology: mode, base and
HEADSHAs or path, timestamp, changed vs context files or mapped vs deep-reviewed files, exclusions with reasons, lenses run and skipped with reasons, and which advisor checkpoints ran - a plain-language summary with counts by severity and by verdict
- findings (
supportedonly), ordered by severity, each with ID, title, lens, severity, finder confidence, verdict, location, attacker and entry point, path to impact, evidence, defences checked, and remediation in prose (no patches) - unresolved claims with the same fields plus what would decide them
- rejected candidates, one line each with the reason
- not reviewed: coverage gaps in repo mode
- limitations
Reports never quote a secret value; secrets are masked with their type and location. They describe vulnerability classes and paths, never runnable exploit payloads. When nothing is found the report says so within the reviewed scope and lenses, without claiming the code is secure.
After writing, Steiner summarises in chat: scope, counts, blockers or coverage gaps, the report path, and a one-line suggestion to add .steiner/security/ to .gitignore if reports should not be committed.
The verifier is the deepest-reasoning step, so the evaluate sub-agent can be assigned a stronger or different model than the rest of the audit:
models:
profiles:
default:
sub_agents:
evaluate: anthropic/<evaluate-model-id>If no override is set, evaluate uses the profile's default_model. See Recommended model tiers for how the agent types map to model tiers.
The audit has no built-in caps: no limit on the number of finders, verifiers, or context files pulled in by the one-hop expansion. Cost and latency scale with the scope you audit, so a whole-repository run on a large codebase can be expensive. Start with a diff or a subtree when in doubt.
Limitations to keep in mind:
- static review only; nothing is executed or dynamically tested
- no CVE or advisory data, so known-vulnerable dependency versions are not matched
- the one-hop context heuristic can miss multi-hop source-to-impact paths
- verifier independence can be limited by configuration
- The only file the audit writes is the report; nothing else is ever created, modified, or deleted.
- When sandboxing is enabled, the mapper and verifiers run with the project mounted read-only. Finders don't get a read-only mount in build mode, so for them read-only is an instruction in the brief, not an enforced restriction. In plan mode the whole project is read-only apart from
.steiner/plans/,.steiner/security/, and.gitwhen sandboxed. - Repository files and tool output are treated as untrusted data, never as instructions.
- No exploit payloads, no secret values, no "secure" claims.
- Execution mode is never switched; plan mode stays plan mode.
Get started
Using Steiner
- How to work with steiner
- Interactive mode
- CLI reference
- Skills
- Sub-agent delegation
- Oneshot
- Context management
- Advisor
- Security audit
Tools and integrations
Safety
Usage and cost
Reference
Internals and maintenance