State-of-the-art security review, driven by the coding CLI you already have.
Claude Code · Codex · OpenCode · Command Code
No new API key. No cloud upload. No third-party scanner. airsec drives the agent already installed on your machine through a structured, multi-pass security audit, then turns the result into a report you can act on.
npx airsec@latest initThat is the whole thing. init walks you through it: pick the folder, pick your coding CLI, pick the
model, choose review-or-fix, and it runs.
Other ways to install
npm install -g airsec # global
pnpm add -g airsec
bun add -g airsec
yarn global add airsec
airsec init # then run it anywhereNo curl | sh installer, deliberately: a tool that audits your code for supply-chain risk should not
ask you to pipe a URL into a shell.
Getting ETARGET · No matching version found?
Your npm is pointed at a mirror that has not synced the release yet. Check with npm config get registry — if it is anything other than https://registry.npmjs.org, either wait for the mirror to
catch up or bypass it:
npx --registry=https://registry.npmjs.org airsec@latest init # one-off
npm config set registry https://registry.npmjs.org # permanently- Node ≥ 20 (the published bundle has zero runtime dependencies)
- At least one supported coding CLI on your
PATH— check withnpx airsec harness
npx airsec harness # 1. what coding CLIs can airsec see?
npx airsec@latest init # 2. guided setup — writes .airsec/config.json, then runs
npx airsec report --format html > report.html # 3. share the result
npx airsec fix --severity high # 4. remediate the worst of itPattern scanners find what patterns catch. An agent that reads your code can follow a tainted value through three helpers and a middleware layer and tell you whether the sink is actually reachable. airsec supplies the missing part: a rigorous process around that agent.
- Reconnaissance first. The attack surface is mapped — entrypoints, trust boundaries, sensitive assets — before any vulnerability hunting starts, and that map is fed into every later pass.
- Fourteen focused audit modules instead of one "find bugs" prompt, so injection analysis is not competing for attention with IaC review.
- Multiple search strategies per module. Forward taint analysis, reverse sink analysis, goal-oriented attack, and invariant enforcement each find things the others miss.
- Adversarial verification. Every finding is handed to independent reviewers whose job is to refute it. Findings that lose the vote are dropped, and you see how many were.
- Deterministic scoring and merge. Deduplication, severity weighting, and the aggregate risk score are plain code, not another model call.
Run airsec harness to see which of these are installed and what models each offers.
| Harness | id | Install |
|---|---|---|
| Claude Code | claude |
npm i -g @anthropic-ai/claude-code |
| Codex | codex |
npm i -g @openai/codex |
| OpenCode | opencode |
npm i -g opencode-ai |
| Command Code | commandcode |
npm i -g command-code |
| Gemini CLI | gemini |
npm i -g @google/gemini-cli — experimental adapter |
| Cursor Agent | cursor |
curl https://cursor.com/install -fsS | bash — experimental adapter |
| Crush | crush |
npm i -g @charmland/crush — experimental adapter |
The first four are built against headless flags verified against the real CLIs, and each is tested end to end in both directions — audit (read-only) and fix (write). The ones marked experimental are best-effort: they are overridable from config without touching the source.
Model ids change every few months. airsec never ships a versioned id that can rot into a wrong
default, so airsec harness --models <id> asks your machine instead:
| Harness | Where the list comes from |
|---|---|
| OpenCode | opencode models — the live list, hundreds of entries |
| Command Code | cmd --list-models — the live list with descriptions |
| Claude Code | your ~/.claude/settings.json model, plus the opus/sonnet/haiku aliases, which resolve to the current model by design |
| Codex | the model in your ~/.codex/config.toml, including profiles |
A probe command is only ever one verified to list models. Guessing is unsafe: several of these CLIs treat an unrecognised subcommand as a prompt, so a wrong guess would bill you for a model call instead of printing a list. Where no list exists, the wizard offers "harness default" plus free-text entry — always correct, never stale.
npx airsec init # asks: which folder, which CLI, which model, review or fix
npx airsec init ../api # start on a specific pathThe wizard writes .airsec/config.json, so every later run is a bare airsec scan.
airsec scan # uses .airsec/config.json, or sensible defaults
airsec scan ../api --depth paranoid
airsec scan --harness codex --model gpt-5.1-codex-max
airsec scan --modules injection,authn-authz,secrets --min-severity medium
airsec scan --quiet --reports sarif --fail-on high --require-complete # CI shapeExit codes only matter when something automated is watching — a CI pipeline decides pass or fail from this number. Running airsec yourself, you can ignore them; the wizard does not ask about them for that reason.
| Exit | Meaning |
|---|---|
0 |
No finding at or above --fail-on |
1 |
airsec itself failed — bad config, harness missing, unreadable target |
2 |
A finding at or above --fail-on (default high) survived verification |
3 |
An audit pass did not finish, and --require-complete was set |
--fail-on sets the bar: --fail-on critical fails a build only on critical findings,
--fail-on none never fails it. It changes nothing about the scan or the report.
Coverage can be partial, and that is not the same as clean. If a harness invocation times out or
errors, airsec records it, finishes the remaining passes, and reports the run — but the areas that
pass covered were never reviewed. Both the terminal output and report.md lead with a warning when
this happens, summary.incompletePasses carries it in JSON, and --require-complete turns it into
a build failure. Never read a partial run as a clean bill of health.
Parallel sessions of the same coding CLI contend for one rate limit, and a stalled session burns the
full --timeout (default 900s) before giving up. Concurrency 5 against Claude Code lost 5 of 14
passes to timeouts in testing; the default of 3 is deliberately conservative. If you see timeouts,
lower --concurrency before raising --timeout.
airsec fix # pick findings from the last review interactively
airsec fix --all --severity high # remediate everything high and aboveFixes are applied to your working tree, one finding at a time — sequentially on purpose, because parallel agents editing overlapping files produce conflicting writes. Review the diff before you commit; airsec never commits for you.
Each fix is scoped to one finding, so a change that alters a function's contract can leave a caller
in another file out of step. The remediation prompt tells the agent to follow the change across file
boundaries, but weaker models do not always manage it — run your tests after fix, always.
Every run is stored under .airsec/runs/<timestamp>-<id>/ with the formats you configured.
airsec report # re-print the last review
airsec report --format html > out.html
airsec report --format sarif > out.sarifSARIF uploads straight to GitHub code scanning.
| Depth | Lenses per module | Verifiers per finding | File budget |
|---|---|---|---|
quick |
1 | 0 | 600 |
standard |
1 | 1 | 2,000 |
deep (default) |
2 | 2 | 6,000 |
paranoid |
3 | 3 | 20,000 |
Cost scales roughly with modules × lenses, plus one verification call per finding per verifier.
Start at quick on a new codebase to calibrate before committing to paranoid.
airsec modules prints these with their CWE and OWASP mappings.
Core (default): injection · authn-authz · secrets · crypto · input-validation ·
ssrf-network · xss-frontend · path-file · supply-chain · infra-config
Extended (opt in): api-surface · logic-race · llm-agentic · privacy-compliance
.airsec/config.json:
{
"version": 1,
"target": "/abs/path/to/repo",
"harness": "claude",
"model": "opus",
"depth": "deep",
"modules": ["injection", "authn-authz", "secrets"],
"concurrency": 3,
"timeoutMs": 900000,
"retries": 1,
"verify": true,
"minSeverity": "low",
"exclude": ["fixtures/", "generated/"],
"reports": ["markdown", "json", "sarif"],
"failOn": "high",
"harnesses": {}
}exclude entries are case-insensitive substring matches against each path relative to the target —
not globs. "generated/" drops the directory; ".test." drops test files anywhere.
When a CLI changes its headless flags, fix it in config rather than waiting for a release. Tokens
{{prompt}}, {{system}}, {{model}} and {{cwd}} are substituted per invocation; an argument
whose token has no value is dropped along with its flag.
{
"harnesses": {
"gemini": {
"bin": "gemini",
"promptVia": "stdin",
"args": {
"read": ["--model", "{{model}}", "--output-format", "text"],
"write": ["--model", "{{model}}", "--yolo"]
}
}
}
}inventory walk the tree, detect stack, rank security-relevant paths (deterministic)
↓
recon one pass: entrypoints, trust boundaries, sensitive assets (1 call)
↓
audit modules × lenses, run concurrently (m × l calls)
↓
merge fingerprint by location, corroborate by title overlap (deterministic)
↓
verify independent reviewers try to refute each finding (f × v calls)
↓
score severity × confidence × verdict × corroboration (deterministic)
↓
report terminal · markdown · json · sarif · html
bun install
bun run dev -- scan ./fixture --depth quick # run from source
bun run check # lint + typecheck + test, what CI runs
bun run format # apply Biome fixes
bun run build # bundle to dist/cli.js, node-compatibleBun is the toolchain; the published bundle targets Node ≥ 20 so npx airsec works anywhere. Biome
enforces formatting and lint, tsc --strict enforces types, and CI runs all three plus the packaged
binary on every push.
Extending it. A new harness is one file in src/harness/adapters/ plus a line in registry.ts —
or zero code, via a config override. A new audit module is one entry in
src/audit/catalog.ts. A new report format is one entry in src/report/index.ts.
src/
audit/ what to look for — module catalog, search lenses, prompts, output schema
core/ the pipeline — inventory, recon, fan-out, merge, verify, score, trust boundary
harness/ how to talk to each coding CLI — adapters, process exec, JSON extraction
report/ terminal · markdown · json · sarif · html
ui/ wizard prompts, progress, banner
util/ fs, json recovery, hashing, redaction, logging
airsec is pointed at code you may not trust, so it treats the target directory as hostile input.
Config from the target is not trusted to decide what runs. .airsec/config.json lives inside
the directory under review. Its harnesses block names an executable and its argv, so honouring it
from a repository you just cloned would be remote code execution. That block is ignored by default,
with a warning, and applied only when you pass --trust-config on the command line — somewhere the
repository cannot reach. Everything else in the file only decides what gets read, and is applied
normally.
Audit passes cannot execute commands or reach the network. The repository under review is attacker-controlled input to a model, so injected instructions in a README or a code comment must not be able to reach a shell (remote code execution) or the network (exfiltration of the source being reviewed). Reconnaissance, auditing and verification run with only the tools an audit needs — open a file, search, list.
For Claude Code that means Bash, WebFetch, WebSearch and Task are denied alongside the
editing tools; Task because a subagent could otherwise be spawned with the tools just removed.
Codex runs under --sandbox read-only, OpenCode under its plan agent, Command Code under
--permission-mode plan. Write access is granted only by airsec fix, and even then network tools
stay denied.
This is verified rather than assumed: a fixture repository carrying prompt-injection text in its README and source is scanned as part of testing, and the injected command does not run.
Discovered credentials are masked in stored output. A secrets finding would otherwise copy a
live credential into report.md, report.json and report.sarif — files that get committed and
pasted into tickets. High-entropy tokens are masked to sk_l…redacted…4c across every text field of
a finding, not just the code snippet: models routinely spell the same credential out again in the
exploit narrative as a URL parameter. The credential stays identifiable, and the file, line, and
exploit path are untouched. Add .airsec/ to your .gitignore regardless.
These properties are covered by test/security.test.ts.
- airsec runs your coding CLI, so it spends that CLI's quota and inherits its permissions. Audit
passes are launched read-only;
fixis the only mode that grants write access. - Findings are produced by a language model. Verification removes a lot of noise, but treat the output as a strong prioritised lead list, not a proof.
- A clean report is not a security guarantee. It is one pass by one reviewer.
- Findings are stored under
.airsec/runs/. Keep that directory out of version control.
MIT