Aegis

Command reference

Every aegis subcommand, flag, exit code, and example. 🌐 marks commands that require the Aegis backend.

Every subcommand aegis --help lists, with flags, examples, exit codes, and output format. Authoritative as of the current main branch.

Legend: 🌐 marks commands that require a reachable Aegis API server (set via AEGIS_API_URL). The hosted Aegis Cloud is not yet available and the platform repo is currently private β€” these commands are documented and shipped, but won’t function for most local users today. Everything else works locally with no backend.

Global flags (apply to every subcommand):

FlagDescription
-v, --verboseEnable debug-level structured logging to stderr (slog DEBUG). Same effect as AEGIS_VERBOSE=1.
-h, --helpShow help for the current command.

Common output flags (where applicable):

FlagDescription
--jsonEmit a machine-readable JSON object to stdout. Suppresses human-readable output entirely. Stable schema β€” safe to parse.
--quietPrint only the summary line (no per-finding detail). Mutually compatible with --json.

Exit codes (uniform across the binary):

CodeMeaning
0Success / no findings β‰₯ threshold
1Failure / findings β‰₯ threshold (block, prompt, etc. depending on --fail-on)
2Couldn’t reach a verdict β€” config error, network error, malformed input

🌐 aegis npm / aegis bun / aegis yarn / aegis pnpm#

Requires Aegis API. Drop-in wrappers around the underlying package manager. Install commands are intercepted, parsed, checked against the Aegis API, and either allowed (passed through to the real PM), prompted (interactive y/N), or blocked (non-zero exit, no PM call). All non-install commands pass straight through.

Without AEGIS_API_URL pointing at a reachable backend, install commands will return a connection error. Non-install passthrough (aegis npm test, aegis bun run dev) works regardless.

aegis npm install lodash@4.17.21      # checked
aegis npm test                        # passthrough
aegis bun add lodash@^4.17.0          # range β†’ resolved via npm registry β†’ checked
aegis yarn global add create-react-app
aegis pnpm add lodash

Install verbs recognized per PM:

PMRecognizedNotes
npminstall, i, add, plus typo aliasesnpm i is the same as npm install
buninstall, i, add, aBun’s bun add foo is the canonical install
yarnadd, install, global addYarn classic + berry; yarn global add is treated as install
pnpmadd, install, iWorkspace-aware via the underlying pnpm

Non-registry installs are detected and passed through without an API check (we have no version to check against):

  • ./vendor/foo (local path)
  • git+https://… (git URL)
  • link:../sibling, workspace:* (workspace protocols)
  • portal:./pkg, patch:foo, exec:node, npm:alias@1.0.0 (yarn-berry protocols)

Interactive prompts use /dev/tty (so they work even when stdin is piped). In CI (CI=true, GITHUB_ACTIONS, etc.), prompts auto-block β€” never wait for input.

Override: pass AEGIS_OVERRIDE=allow and AEGIS_OVERRIDE_REASON='<text>' to bypass a block; both are written to the audit log. An empty reason is refused.

Exit codes: 0 if the install proceeds (allow / approved prompt / passthrough), 1 if anything was blocked.


aegis snapshot save#

Scan the project’s lockfile(s) and write aegis.lock at the project root. No network calls β€” pure lockfile parse + serialise. Fast, deterministic, safe to commit.

aegis snapshot save                   # auto-detect lockfile(s)

Recognised lockfiles (every match in the project root is parsed; deps are merged in one snapshot keyed by ecosystem):

EcosystemLockfiles (priority order within the ecosystem)
npmpnpm-lock.yaml β†’ yarn.lock β†’ bun.lock β†’ package-lock.json
PyPIpoetry.lock β†’ uv.lock β†’ Pipfile.lock β†’ requirements.txt
crates.ioCargo.lock
Gogo.sum
RubyGemsGemfile.lock

Within an ecosystem, only the first match is parsed (a project typically commits to one tool). Across ecosystems, every match is parsed β€” polyglot monorepos with both package-lock.json and Cargo.lock produce a single aegis.lock containing both.

Output: writes ./aegis.lock (zstd-compressed JSON). Idempotent β€” repeated saves with no lockfile change produce a byte-identical file.

Exit codes: 0 on success, 2 if no lockfile is found.


aegis snapshot show#

Print the saved snapshot. By default only direct deps are shown (transitive deps are in the file but hidden from the table by default).

aegis snapshot show                        # direct only
aegis snapshot show --all                  # include transitive
aegis snapshot show --all --used-only      # hide deps not referenced by project source
aegis snapshot show --json                 # full JSON output including reach + symbols
FlagDefaultDescription
--alloffInclude transitive dependencies in the rendered table.
--used-onlyoffHide deps whose reachability is unused β€” in the lockfile but not imported by project source. Requires a prior snapshot enrich run. Filtered count shown in footer.
--jsonoffEmit the full snapshot as a JSON array. Includes all fields including reach and symbols.

Output columns:

ColumnMeaning
ECOEcosystem (npm, pypi, crates, go, rubygems)
NAMEPackage name
VERSIONResolved version
DIRECTβœ“ when listed in the project manifest (vs transitive)
CAPSAST + heuristic capability count. Empty if not enriched yet; β€” if enriched with no findings. Appends [unused] when reachability scan found no import of this dep in project source.
ADVISORIESOSV.dev vulnerability count + max severity, color-coded. Empty if not looked up; β€” if looked up with no matches

aegis snapshot diff [a.lock] [b.lock]#

Diff two snapshots. With no arguments, diffs the saved aegis.lock against the live lockfile. With one argument, diffs aegis.lock (saved) against the given path. With two, diffs the two files.

aegis snapshot diff                              # saved vs live
aegis snapshot diff baseline.lock                # baseline vs current saved
aegis snapshot diff main.lock pr-branch.lock     # explicit

Reports added, removed, upgraded, downgraded β€” and drift (a version-changed dep that grew new capabilities). Drift is the high-signal entry: lodash 4.17.20 β†’ 4.17.21 is normal, but if 4.17.21 newly contains child-process it’s worth a look.


aegis snapshot enrich#

Run AST analysis + vulnerability lookup over every dep in the saved snapshot.

Two phases per run:

  1. AST scan (parallel, 8-worker pool): fetch the tarball from the registry (cached under ~/.aegis/cache/sources/), gunzip and untar in memory, walk the tree-sitter AST, and write capability fingerprints back into aegis.lock. Per-dep cost: 100ms–2s for first scan, ~5ms cache hit on subsequent runs.
  2. Vulnerability lookup (single batch POST to OSV.dev): every dep is cross-referenced against the public OSV vulnerability database. Returned advisories are stamped onto each Dependency and persisted in aegis.lock. Advisory bodies are cached under ~/.aegis/cache/advisories/. No Aegis API required, no auth needed.
aegis snapshot enrich

Disable the vulnerability lookup with AEGIS_NO_VULN_LOOKUP=1 (AST scanning still runs). Point at a self-hosted OSV mirror with AEGIS_OSV_URL=…. Respects AEGIS_CACHE_DIR.

Live progress UI: when stderr is a TTY and AEGIS_NO_LIVE is unset, shows an 8-slot live status panel. Disabled in CI and when piped.


🌐 aegis snapshot submit#

Requires Aegis API. Post analyzed deps as community reports to the Aegis API. Requires AEGIS_API_KEY β€” keys are issued via the Aegis web UI under /admin?tab=api-keys.

AEGIS_API_KEY=… aegis snapshot submit

Submits one report per (ecosystem, name, version) tuple in the saved snapshot. The API deduplicates server-side; resubmitting doesn’t create duplicate records. Submit failures are logged but don’t fail the command β€” best-effort.


aegis snapshot verify#

Check that aegis.lock is loadable and matches the current schema. Used by CI to catch corrupted or out-of-date snapshot files before they trip enrich.

aegis snapshot verify

Exit codes: 0 if loadable, 2 if missing / malformed / schema-incompatible.


aegis ci#

One-stop CI command. Runs the full audit pipeline: snapshot save β†’ snapshot enrich (AST scan + OSV vulnerability lookup; skippable with --no-enrich) β†’ score β†’ exit.

Scoring folds two signals: AST capability findings (suspicious code patterns) and known vulnerabilities from OSV.dev (CVE / GHSA). Verdict is max(astVerdict, advisoryVerdict):

SourceCritical / HighMediumLowInfo
Advisory severityblockpromptreviewsafe
AST score(capability-weighted; see Risk engine)
aegis ci --fail-on=block                      # default
aegis ci --fail-on=prompt                     # tighter
aegis ci --fail-on=review                     # tightest (warn-level fails)
aegis ci --json | jq '.findings[] | .name'    # machine-readable
aegis ci --baseline=baseline.lock             # drift mode
aegis ci --no-enrich                          # score on existing fingerprints only
aegis ci --scan-actions                       # also scan .github/workflows/
aegis ci --sarif > packages.sarif             # SARIF 2.1.0 output
FlagDefaultDescription
--fail-onblockThreshold to fail on: safe (any finding) β—€ review β—€ prompt β—€ block (only blocks)
--jsonoffEmit a JSON object to stdout. Suppresses human output.
--quietoffPrint only the summary line.
--no-enrichoffSkip the AST scan; score on existing fingerprints. Faster, thinner.
--baseline <path>(none)Drift mode: diff against this saved snapshot, only fail on newly-introduced findings. Doesn’t touch your aegis.lock.
--scan-actionsoffAlso scan .github/workflows/. Threshold controlled by --actions-fail-on. Respects .aegis-actions-allowlist.yaml.
--actions-fail-onhighMinimum severity for --scan-actions: low|medium|high|critical
--sarifoffEmit SARIF 2.1.0. When combined with --scan-actions, both package and workflow findings are emitted as two runs[] in one SARIF file.
--suggestoffPrint per-ecosystem upgrade commands for each blocked dep (npm install pkg@latest, pip install --upgrade pkg, etc.).

The fingerprint cache (~/.aegis/cache/fingerprints/) persists across runs β€” a warm CI is fast. Only newly-added or version-changed deps incur AST scan cost.

Exit codes: 0 if no findings β‰₯ --fail-on, 1 if any, 2 on config / network errors.

GitHub Actions β€” full audit in one step:

- name: Audit dependencies + workflows
  run: aegis ci --scan-actions --sarif > aegis.sarif

- name: Upload to GitHub Security
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: aegis.sarif
  if: always()

🌐 aegis recheck#

Requires Aegis API. Re-run the install gate against the current lockfile. Useful after an incident DB update β€” packages allowed at install time may now be flagged.

aegis recheck                       # direct deps only
aegis recheck --all                 # include transitive
aegis recheck --json
FlagDefaultDescription
--alloffInclude transitive deps (default: only direct, matching what the user explicitly installed)
--fail-on-promptoffExit non-zero on prompt verdicts (default: only block fails)
--jsonoffEmit JSON to stdout
--quietoffSummary line only

aegis analyze <pkg-spec>#

Fetch and AST-scan a single package β€” fallback when the incident DB has no record. Spec is [ecosystem/]name@version. Default ecosystem is npm. Recognised ecosystem prefixes: npm, pypi, rubygems, crates, go. The registry-fetch path supports npm today; the other ecosystems work through --local.

aegis analyze lodash@4.17.21
aegis analyze @solana/web3.js@1.95.4
aegis analyze npm/event-stream@3.3.6
aegis analyze --evidence ua-parser-js@0.7.29       # show file:line snippets
aegis analyze --json lodash@4.17.21

# --local skips the registry fetcher and reads source from the
# on-disk directory at <path>. Spec is still required as a label.
aegis analyze rubygems/rest-client@1.6.13 \
  --local examples/incidents/rubygems/rest-client-1.6.13/
FlagDescription
--evidenceInclude file:line snippets for each detected capability
--jsonEmit JSON to stdout (suppresses human output)
--local <path>Skip the registry fetcher and read package source from <path>. Useful for fixture-based testing and pre-publish self-checks. The spec (<eco>/<name>@<version>) is still required as a label.
--ecosystem <eco>Treat the positional argument as a directory for the given ecosystem instead of a <name>@<version> spec. Currently implemented for neovim β€” plugins have no registry and no manifest, so Name is derived from the directory basename and Version from the git HEAD SHA.
--baseline <prior.json>Compare the current scan’s capabilities against a previously-saved --json output. Exits 1 when new capabilities appear. Capability shrinkage is fine; capability growth is a regression. Plugin managers use this to detect β€œdid this update get worse?” on plugin SHA bumps.

The --local mode runs the same AST + heuristics pipeline snapshot enrich does, so the capability set is identical for the same input. Real-world incident fixtures ship under examples/incidents/ (rubygems/, pypi/, npm/, crates/, go/) and are validated by tests/e2e/incidents.sh on every CI run.

Neovim plugin scanning#

# Scan a local Neovim plugin checkout (no registry, no manifest)
aegis analyze --ecosystem neovim ./packer.nvim

# Plugin-manager integration: cache the JSON baseline on install,
# re-scan on update, fail if new capabilities appeared.
aegis analyze --ecosystem neovim ./plugins/foo --json \
  > ~/.cache/aegis/foo-<sha>.json
aegis analyze --ecosystem neovim ./plugins/foo \
  --baseline ~/.cache/aegis/foo-<old-sha>.json

Coverage maps Lua AST β†’ existing capabilities: os.execute / vim.fn.system β†’ shell-spawn, loadstring / vim.api.nvim_exec β†’ dynamic-eval, require("socket.http") / vim.uv.new_tcp β†’ net-egress, os.getenv / vim.env.* β†’ env-read, io.open / vim.fn.writefile β†’ fs-write-outside-root, ffi.load / package.cpath = ... β†’ install-hook-exec. Plugin spec build = "<shell>" strings feed the same matcher that flags curl | sh in npm scripts. No OSV ecosystem for Neovim exists; --enrich returns nothing β€” this path is static-only. See docs/neovim-plugin-manager-safety-spec.md for the full plugin-manager integration spec.


aegis explain <pkg-spec>#

Explain why a dep was flagged. Looks up the dep in the saved aegis.lock first (no network); falls back to a fresh fetch + AST scan if not present.

aegis explain lodash@4.17.21
aegis explain --snapshot-only ua-parser-js@0.7.29   # error if not in lock
aegis explain --json lodash@4.17.21
FlagDescription
--snapshot-onlyOnly consult saved aegis.lock; never fetch + rescan. Errors if the dep isn’t in the snapshot.
--jsonEmit JSON to stdout

Each capability is rendered with a one-line description; allowlist suppression reasons are surfaced; evidence (file:line) is shown when the scan was fresh.


aegis allowlist#

Manage capability suppressions for specific packages. Layered: builtin β†’ user (~/.aegis/allowlist.yaml) β†’ project (./.aegis-allowlist.yaml). Specific names beat wildcards; within each layer input order decides ties.

aegis allowlist list#

aegis allowlist list                              # all rules from all sources
aegis allowlist list --source=builtin             # filter by source
FlagDescription
--sourceFilter by source: builtin / user / project

aegis allowlist add <name>#

aegis allowlist add lodash \
    --capability=dynamic-eval \
    --version='^4' \
    --reason='_.template uses Function() to compile templates'
FlagDefaultDescription
--ecosystemnpmnpm / pypi / crates / go / maven
--capability(any)Capability code to suppress; omit for β€œany capability”
--version(any)Semver range to scope the rule to; omit for β€œany version”
--reason(none)Explanation; strongly recommended. The allowlist is an audit trail β€” empty reasons are worse than no rule.
--scopeuseruser (~/.aegis/allowlist.yaml) or project (./.aegis-allowlist.yaml)

aegis allowlist remove <name>#

aegis allowlist remove lodash --capability=dynamic-eval
aegis allowlist remove lodash --scope=project
FlagDefaultDescription
--ecosystemnpmRequired to disambiguate cross-ecosystem rules
--capability(all)Narrow removal to a single capability; omit to remove all rules for the name
--scopeuseruser or project

aegis allowlist test <ecosystem>/<name>@<version>#

Show which allowlist rules would suppress capabilities for a given (ecosystem, name, version) tuple. Doesn’t fetch or scan β€” pure rule evaluation.

aegis allowlist test npm/lodash@4.17.21

aegis allowlist verify#

Validate user and project allowlist YAML files. Strict decoding: unknown keys, unknown capabilities, and unsupported schema versions all error out.

aegis allowlist verify

🌐 aegis allowlist sync#

Requires Aegis API. Fetch the org-level allowlist overlay from the Aegis API and cache it locally at ~/.aegis/cache/org-allowlist.yaml. Requires AEGIS_API_KEY.

AEGIS_API_KEY=… aegis allowlist sync
aegis allowlist sync --force                     # ignore cache freshness
FlagDescription
--forceBypass the cache freshness check and re-fetch unconditionally

aegis cache#

aegis cache list#

List cached decisions (the ~/.aegis/cache/decisions.json map of (eco, name, version) β†’ verdict). Useful for β€œwhy was this allowed?” debugging.

aegis cache list

aegis cache clear#

Delete the local decision cache. Pass --fingerprints to also delete the AST fingerprint cache (~/.aegis/cache/fingerprints/); pass --all for both plus the package source cache (~/.aegis/cache/sources/).

aegis cache clear                                # decisions only
aegis cache clear --fingerprints                 # + fingerprints
aegis cache clear --all                          # everything
FlagDescription
--fingerprintsAlso delete AST fingerprint cache
--allDelete decisions + fingerprints + tarball sources

aegis audit tail#

Show the most recent entries from the local audit log (~/.aegis/audit.jsonl). One line per outcome (allow / block / override / …) with timestamp, package, decision, and reason.

aegis audit tail                  # last 20
aegis audit tail -n 100           # last 100
aegis audit tail -n 0             # all
FlagDefaultDescription
-n, --n20Show the last N entries; 0 means all

aegis hook#

aegis hook install#

Install the aegis pre-commit hook in the current git project. The hook runs aegis ci --fail-on=block before each commit.

aegis hook install

Writes .git/hooks/pre-commit. Refuses to overwrite an existing hook unless you remove it first.

aegis hook uninstall#

Remove the aegis pre-commit hook. Idempotent.

aegis hook uninstall

aegis doctor#

Sanity-check the local environment: API reachability, cache permissions, allowlist parse, free disk. Run this first when something seems off.

aegis doctor
aegis doctor --json
FlagDescription
--jsonEmit machine-readable JSON

Checks performed:

  1. API reachability β€” HEAD AEGIS_API_URL/check; reports the HTTP status (any code = reachable)
  2. Cache directory β€” ~/.aegis/cache/ is writeable
  3. Allowlist parse β€” user + project YAML files load without errors
  4. Disk space β€” at least 100MB free on the cache filesystem
  5. Build info β€” Go version, OS/arch, build tags

Exit codes: 0 if everything green, 1 if any check failed, 2 if doctor itself crashed.


aegis actions scan#

Scan GitHub Actions workflows for supply-chain risks β€” locally or from a remote repository.

# Scan current project's .github/workflows/
aegis actions scan

# Scan a remote repository (uses $GITHUB_TOKEN automatically)
aegis actions scan --repo owner/repo

# Emit SARIF 2.1.0 for GitHub Code Scanning
aegis actions scan --sarif > results.sarif

Flags

FlagDefaultDescription
--min-severityhighMinimum severity to fail on: low|medium|high|critical
--fail-onβ€”Deprecated alias for --min-severity; prints a warning
--jsonfalseJSON output
--sariffalseSARIF 2.1.0 output (GitHub Code Scanning / VS Code)
--dircwdProject root; ignored when --repo is set
--repoβ€”Remote GitHub repo (owner/repo) via GitHub Contents API
--tokenβ€”GitHub PAT; prefer $GITHUB_TOKEN (CLI args visible in process list)

Exit codes: 0 clean, 1 findings β‰₯ --min-severity, 2 I/O error.

Detections

FindingSeverity
unpinned_refHigh / Medium
pull_request_target_checkoutCritical
write_all_permissionsHigh
script_injectionCritical
suspicious_runHigh
oidc_npm_publishHigh
cache_poisoningHigh

Allowlist β€” suppress findings via .aegis-actions-allowlist.yaml:

version: 1
rules:
  - kind: unpinned_ref
    file: .github/workflows/release.yml
    reason: "managed by dependabot"
  - kind: "*"
    file: .github/workflows/legacy.yml
    reason: "legacy workflow not in production path"

Suppressed findings are still shown in output but don’t trigger --fail-on.

Upload SARIF to GitHub Security tab

- run: aegis actions scan --sarif > aegis-actions.sarif
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: aegis-actions.sarif
  if: always()

aegis image scan <image.tar>#

Scan a local OCI / Docker image tarball for supply-chain risks. Reads docker save output (or any OCI-format archive), overlays every layer (whiteout-aware), and extracts every dependency it can find via two complementary paths:

  1. Lockfiles baked into the image β€” package-lock.json, Gemfile.lock, Pipfile.lock, composer.lock, etc. Parsed by the same locksnap registry the project-mode scanner uses
  2. Per-package manifests that live outside any lockfile β€” node_modules/<pkg>/package.json, *.dist-info/METADATA, *.egg-info/PKG-INFO, gems/<name>-<ver>/, vendor/<v>/<p>/composer.json, and /opt/<tool>-v<ver>/package.json (top-level npm tooling like yarn). Closes the recall gap on distroless and multi-stage images where the lockfile never lands in the final image
docker save my-app:latest -o my-app.tar
aegis image scan my-app.tar
aegis image scan my-app.tar --enrich
aegis image scan my-app.tar --capabilities
aegis image scan my-app.tar --no-manifest-walk     # lockfile-only mode
aegis image scan my-app.tar --json | jq '.deps[].name'

Flags

FlagDefaultDescription
--enrichfalseRun OSV.dev vulnerability lookup against the extracted dependency set
--capabilitiesfalseAST-scan every package found inside the image (tree-sitter capability + heuristic detection β€” finds malware Trivy can’t)
--no-manifest-walkfalseSkip per-package manifest scanning, return lockfile-derived deps only
--jsonfalseMachine-readable JSON on stdout

Provenance β€” every dep in the JSON output carries a source field: "lockfile" (parsed from a known lockfile), "manifest" (synthesized from a per-package manifest), or omitted for older snapshots. Use it to audit how a particular dep was discovered.

Recall β€” example numbers from public base images:

ImageLockfile-onlyManifest walker ON
node:20-alpine0193 npm
ruby:3.3-alpine52 gems130 gems
python:3.12-alpine01 pypi

Combine --enrich with manifest walk to surface CVEs that lockfile-only scans miss entirely β€” e.g. cross-spawn ReDoS and glob command injection on the stock node:20-alpine image.

Memory β€” each captured manifest is capped at 64 KB and the total per-image manifest budget is 64 MB (β‰ˆ 32k packages). When the cap fires the result is partial and truncated: true lands in the JSON output.

Exit codes: 0 clean scan, 2 I/O error reading the tar.


aegis admin gen-key#

Generate a fresh submit API key plus a sha256 hex digest for installing it server-side. Used when bootstrapping a new operator account against a self-hosted Aegis API.

aegis admin gen-key

Output: two lines β€” the key (give to the user) and the sha256 hex (insert into the submit_api_keys table). The key itself is never stored server-side.


aegis version#

Print the binary version, commit hash, and build date.

$ aegis version
aegis 0.1.0 (commit 6c5844916d8831d841edb2fec1e9dbd615519e9c, built 2026-05-03T04:46:04Z)

All three values are stamped at build time via -ldflags=-X. A binary built locally with plain go build (no ldflags) reports 0.1.0-demo (commit none, built unknown).


aegis completion {bash|zsh|fish|powershell}#

Generate a shell completion script. Pipe to your shell or write to the canonical completions directory:

# Bash (current shell only)
source <(aegis completion bash)

# Bash (persistent, Linux)
aegis completion bash > /etc/bash_completion.d/aegis

# Zsh
aegis completion zsh > "${fpath[1]}/_aegis"

# Fish
aegis completion fish > ~/.config/fish/completions/aegis.fish

# PowerShell
aegis completion powershell | Out-String | Invoke-Expression

Completes subcommand names, flag names, and (where applicable) flag values. The package-manager wrappers (aegis npm, aegis bun, etc.) use DisableFlagParsing and pass argv through unchanged, so completion of their inner args is delegated to the underlying tool.