AgentShield
Security layer for AI agent frameworks. AgentShield intercepts package installation requests made by AI agents, checks them against CVE databases and static analysis tools, enforces configurable response policies, and generates security posture reports — all locally, with no telemetry.
AI agent? Skip straight to the Agent Setup Guide — it has everything your agent needs to integrate AgentShield in one self-contained document.
Why this exists
AI agents can now install arbitrary packages on behalf of users. This creates a novel attack surface that existing security tooling doesn't address:
- An agent can be prompt-injected — a malicious web page or tool result instructs the agent to install a backdoored package
- Agents may typosquat — suggest
requetsinstead ofrequests, orpandainstead ofpandas - Agents don't inherently check CVEs or audit dependency trees before installing
- Compromised packages can exfiltrate context windows, API keys, tool credentials, or local files before the user notices anything
AgentShield sits between the agent's intent ("install X") and the system executing that intent, providing a security checkpoint the agent cannot bypass. It works with any framework through native plugins (Hermes, OpenClaw) or the MCP protocol.
Table of Contents
- Architecture
- Threat model
- Installation
- Quick start
- Configuration
- CLI reference
- Framework integrations
- Posture reports
- Python API
- Static analysis (--deep)
- Transitive dependency scanning
- SBOM generation
- License compliance scanning
- pre-commit hook
- GitHub Action
- Drift detection
- Rate limits
- Diff scan mode
- Trust score / reputation system
- Container / Docker scanning
- HTTP daemon mode
- agentshield guard
- Offline mode
- Caching
- Audit log
- Provenance / attestations
- SARIF output
- Waivers
- Notifications
- Testing
- Contributing
- License
Architecture
┌───────────────────────────────── Entry points ─────────────────────────────────┐
│ │
│ Agent plugins (in-process) Agent hooks Servers / CLI │
│ ┌───────────────────────┐ ┌────────────────────┐ ┌─────────────────────────┐ │
│ │ Hermes pre_tool_call │ │ Claude Code, Codex │ │ MCP (stdio) · HTTP :8765│ │
│ │ OpenClaw │ │ PreToolUse │ │ IPC socket · CLI / CI │ │
│ │ before_tool_call │ │ (agentshield hook) │ │ GitHub Action·pre-commit│ │
│ └───────────┬───────────┘ └─────────┬──────────┘ └────────────┬────────────┘ │
└──────────────┼────────────────────────┼──────────────────────────┼──────────────┘
│ │ │
┌──────────────┴────────────────────────┴──────────────────────────┴──────────────┐
│ Enforcement layers (all fail CLOSED) │
│ guard shell wrapper · PATH shim · execve interceptor (LD_PRELOAD/dyld) │
│ index proxy :8799 — PIP_INDEX_URL / npm registry / GOPROXY │
└──────────────────────────────────────┬──────────────────────────────────────────┘
│ ScanRequest
┌──────────▼──────────┐
│ Core Engine │
│ denylist → allowlist│
│ → cache → rate limit│
└──────────┬──────────┘
│ cache miss
┌─────────────────────────────┼─────────────────────────────┐
│ │ │
┌────────▼─────────┐ ┌───────────▼───────────┐ ┌──────────▼──────────┐
│ Enrichment │ │ Local heuristics │ │ Static analysis │
│ (parallel, fails │ │ (offline-capable) │ │ (--deep only) │
│ open) │ │ │ │ │
│ • OSV │ │ • typosquat (T1.2) │ │ • semgrep │
│ • NVD (CPE- │ │ • malicious DB (T1.1) │ │ • bandit │
│ version-aware) │ │ • prompt-inj. (T4.1) │ │ • AST inspector │
│ • GitHub Advisory│ │ • drift (D1.1) │ │ (PyPI wheels/ │
│ • license (L1.1) │ │ • lockfile hashes │ │ sdists) (T3.x) │
│ • provenance │ │ (H1.x) │ └─────────────────────┘
│ (T6.x) │ │ • syspkg CVEs (SP1.x) │
│ • trust score │ └───────────────────────┘
│ (T5.1) │
└──────────────────┘
│
┌────────▼────────────────────────────────────────────────┐
│ Response Engine │
│ severity policy → per-ecosystem → per-rule-ID → waivers │
│ ⇒ ALLOW · BLOCK · NEEDS_CONFIRMATION · LOG_ASYNC │
└────────┬────────────────────────────────────────────────┘
│ every decision
┌────────▼─────────────────────────┐ ┌──────────────────────────────────┐
│ Audit log — hash-chained JSONL │ │ Local SQLite │
│ + SQLite query index │ │ scan cache · CVE mirror · │
│ + HTTP/syslog forwarding │ │ malicious DB · scan history · │
│ Webhook notifications (Slack) │ │ provenance history · rate limits │
└──────────────────────────────────┘ └──────────────────────────────────┘
Data flow
Agent: "pip install numpy==1.24.0"
│
▼
[Entry point] plugin hook / PreToolUse / guard / shim / execve / proxy / CLI
└─→ ScanRequest(package="numpy", version="1.24.0", ecosystem="pypi")
│
▼
[Core Engine]
├── denylist → BLOCK immediately (never scanned)
├── allowlist → ALLOW immediately (never scanned)
├── cache HIT → return cached ScanResult (< 5 ms; BLOCK verdicts never expire)
└── cache MISS →
├── [Rate limits] packages/hour + per-session wheel budget (R1.1)
├── [Enrichment ∥] OSV + NVD + GitHub Advisory — version-filtered when
│ pinned — plus license policy (L1.1), provenance
│ attestations (T6.x), trust score (T5.1)
├── [Typosquat] Levenshtein vs. top-N package list (T1.2)
├── [Malicious] curated DB + warmed OSV malicious feed (T1.1)
├── [T4.1] prompt-injection heuristic on context_hint
├── [--deep] download wheel → semgrep + bandit + AST (T3.x)
├── [Drift] decision regressed since last scan? (D1.1)
└── [--verify-hashes, scan-file] recorded lockfile hashes vs.
live registry digests (H1.x)
│
▼
[Response Engine] severity policy → ecosystem → rule overrides
│ → waivers (time-boxed, annotated, never silent)
┌─────┴──────┐
│ │
ALLOW BLOCK / NEEDS_CONFIRMATION / LOG_ASYNC
│ │
└─────┬──────┘
▼
[Cache write] severity-based TTL (3 h critical → 7 d clean);
│ BLOCK cached with no expiry (never silently flips to ALLOW)
▼
[Audit log] hash-chained record for EVERY decision — including
│ short-circuits and cache hits (+ webhook notification)
▼
[Entry point] → decision returned to the framework — fail closed on errors
Design principles
- Local-first. The SQLite cache, CVE mirror, and malicious-package list are all on disk. Core scans work without network after
cache warm. No telemetry, no cloud dependency. - Enrichment fails open; enforcement fails closed. When an enrichment source (OSV/NVD/GitHub) times out or errors, it's skipped and logged at WARNING — the scan continues with remaining sources. But the enforcement layers (hooks, guard, shim, execve, proxy) fail closed: a detected install that cannot be verified — unanalyzable arguments, an unsupported source, or a scanner error — is blocked, not allowed through.
- Static analysis is opt-in.
--deepdownloads the wheel and runs semgrep/bandit. Default scans (CVE + typosquat) run in < 3 seconds without downloading anything. - Policy over hard-coding. Every response (block/warn/ignore/log) is driven by the config. You can tune per-severity, per-ecosystem, or per-rule-ID.
Threat model
Informed by "A Security Analysis of the OpenClaw AI Agent Framework" (arXiv 2603.27517), adapted for supply-chain attack vectors.
T1 — Supply Chain Attacks
| ID | Threat | Description |
|---|---|---|
| T1.1 | Malicious package | Package exists solely to exfiltrate data or execute malicious code |
| T1.2 | Typosquatting | Name is a near-miss of a legitimate package (reqests vs requests) |
| T1.3 | Dependency confusion | Internal package name shadowed by a public registry package |
| T1.4 | Compromised package | Legitimate package with a malicious version injected post-publish |
T2 — Known Vulnerabilities (CVEs)
| ID | Threat | Description |
|---|---|---|
| T2.1 | Critical CVE | CVSS ≥ 9.0 in the requested version |
| T2.2 | High CVE | CVSS 7.0–8.9 in the requested version |
| T2.3 | Transitive CVE | Vulnerability in a dependency of the requested package |
| T2.4 | Outdated package | Newer version available with security fixes |
T3 — Install-time Code Red Flags (--deep)
| ID | Threat | Detected by |
|---|---|---|
| T3.1 | Shell execution | subprocess, exec, eval, os.system in setup.py |
| T3.2 | Network at install time | urllib.request, requests, socket calls in setup.py |
| T3.3 | Filesystem write outside package dir | Writes to ~/.ssh, ~/.aws, /etc at install |
| T3.4 | Obfuscated code | exec(base64.b64decode(...)), marshal/zlib chains |
| T3.5 | Credential harvesting | Reads *_KEY, *_TOKEN, *_SECRET env vars at install |
T4 — Agent-Specific Risks
| ID | Threat | Coverage |
|---|---|---|
| T4.1 | Prompt-injected install | Heuristic: flags package names in quoted/code-block patterns in context_hint |
| T4.2 | Excessive tool permissions | Posture report: tool risk classification |
| T4.3 | Context exfiltration risk | Posture report: sensitive env var detection |
Severity and response defaults
| Severity | CVSS range | Default response |
|---|---|---|
| CRITICAL | ≥ 9.0 | block |
| HIGH | 7.0–8.9 | warn_confirm |
| MEDIUM | 4.0–6.9 | async_report |
| LOW | 0.1–3.9 | ignore |
| INFO | 0.0 | ignore |
All defaults are overridable per severity, ecosystem, or rule ID in config.toml.
Installation
pip install git+https://github.com/mkarvan/AgentShield.git
Python 3.11+ required.
Note: PyPI publishing is planned for a future release.
Optional extras
# Static analysis (semgrep + bandit) — needed for --deep flag
pip install "agentshield[static-analysis] @ git+https://github.com/mkarvan/AgentShield.git"
# Hermes Agent integration
pip install "agentshield[hermes] @ git+https://github.com/mkarvan/AgentShield.git"
# OpenClaw integration is a Node plugin (OpenClaw is TypeScript), installed in
# the OpenClaw box — not a Python extra:
# openclaw plugins install @agentshield/openclaw-plugin
# (it shells out to the `agentshield` CLI, so install that too: pipx install agentshield)
# Hermes + static analysis (the [all] bundle)
pip install "agentshield[all] @ git+https://github.com/mkarvan/AgentShield.git"
Quick start
# 1. Scan a package (online — hits OSV + NVD + GitHub Advisory)
agentshield scan requests==2.28.0 --ecosystem pypi
# 2. Deep scan — download wheel and run static analysis
agentshield scan some-new-package --ecosystem pypi --deep
# Scan package + its transitive dependencies
agentshield scan flask --transitive
# Scan an entire requirements.txt at once
agentshield scan-file requirements.txt
# Generate a CycloneDX SBOM from a manifest
agentshield sbom requirements.txt
# 3. Populate local database for offline use (~2–5 min first run)
agentshield cache warm
# 4. Generate a security posture report
agentshield posture
# 5. Start the MCP server (any MCP-compatible agent connects to this)
agentshield serve --mcp
Exit codes for agentshield scan: 0 = ALLOW/WARN/LOG_ASYNC, 1 = BLOCK.
API keys (optional but recommended)
Set these to raise NVD rate limits and enable the GitHub Advisory Database:
export NVD_API_KEY=your-key-here # 5 → 50 req/30s; get one at nvd.nist.gov/developers
export GITHUB_TOKEN=ghp_... # enables GitHub Advisory lookups; any classic PAT works
You can also set them in ~/.config/agentshield/config.toml under [api].
Configuration
AgentShield looks for config at ~/.config/agentshield/config.toml. Create it to override defaults.
[!IMPORTANT] System-package CVE scanning is OFF by default (since v0.9.0). AgentShield still detects system installs (
apt/yum/apk/brew/snap/…) and prints anSP1.1warning, but it does not run a live CVE scan of them unless you opt in. This is deliberate: distro packages ship with many low/medium CVEs, so scanning everyapt-get install curloryum install httpdwould block or nag on routine installs (and slow paths likesnap installcould time out).To turn it on, add three lines to your config:
[syspkg] cve_scan = true # default: falseSee System package scanning (
[syspkg]) for the severity floor, findings cap, and recommended policy.
Full config reference
# ── Response defaults (by severity) ──────────────────────────────────────────
[defaults]
critical = "block" # ALLOW | BLOCK | WARN_CONFIRM | ASYNC_REPORT
high = "warn_confirm"
medium = "async_report"
low = "ignore"
info = "ignore"
# ── Per-ecosystem overrides ───────────────────────────────────────────────────
[ecosystems.pypi]
high = "block" # Stricter than default for pip installs
[ecosystems.npm]
high = "warn_confirm"
critical = "block"
[ecosystems.cargo]
critical = "block"
high = "warn_confirm"
# ── Per-rule-ID overrides (highest priority) ──────────────────────────────────
[rules]
[rules."T1.1"] # Known-malicious: always block, regardless of severity
mode = "block"
[rules."T1.2"] # Typosquatting: always block
mode = "block"
[rules."T2.3"] # Transitive CVEs: only log, don't block
mode = "async_report"
[rules."T3.1"] # Shell execution at install time
mode = "warn_confirm"
[rules."T3.5"] # Credential harvesting: block
mode = "block"
[rules."T4.1"] # Prompt injection heuristic: confirm before allowing
mode = "warn_confirm"
# ── Allowlist / denylist ─────────────────────────────────────────────────────
[allowlist]
# Packages that bypass all checks (trusted internal packages, etc.)
packages = ["numpy", "requests", "pytest", "boto3"]
[denylist]
# Packages that are always blocked regardless of findings
packages = ["malicious-pkg-example", "colouredlogs"]
# ── API keys ─────────────────────────────────────────────────────────────────
[api]
# Also accepted via environment variables NVD_API_KEY and GITHUB_TOKEN
nvd_api_key = "" # Increases NVD rate limit from 5→50 req/30s
github_token = "" # Required for GitHub Advisory Database (GraphQL)
# ── Cache settings ────────────────────────────────────────────────────────────
[cache]
db_path = "~/.agentshield/agentshield.db"
ttl_hours = 24
# Entries are evicted (oldest first) once this many are cached, and each is
# re-fetched after ttl_hours. Never-expiring BLOCK entries are exempt from
# eviction. Entries are small JSON blobs, so the default 50,000-entry cap
# corresponds to roughly tens of MB of disk and working memory.
max_entries = 50000
# Warm-data freshness: offline scans and `cache stats` warn when the OSV
# mirror (populated by `agentshield cache warm`) is older than this.
warm_max_age_days = 7
# Opt-in: long-running `agentshield serve` daemons re-warm stale ecosystems
# in the background, checking every auto_warm_interval_hours.
auto_warm = false
auto_warm_interval_hours = 24
# ── System packages (apt/yum/apk/brew/snap/…) ─────────────────────────────────
[syspkg]
enabled = true # Detect system installs + emit SP1.1 warning (never blocks)
cve_scan = false # OPT-IN live CVE scan of system packages (off by default)
severity_floor = "HIGH" # When cve_scan is on, ignore findings below this severity
max_findings = 50 # Cap findings shown; overflow summarised as "+N more"
# Only applies when cve_scan = true.
[syspkg.severity_policy]
critical = "block"
high = "warn_confirm"
medium = "async_report"
low = "ignore"
info = "ignore"
# ── Release-age quarantine (A1.1) ─────────────────────────────────────────────
[release_age]
min_days = 0 # quarantine versions younger than N days; 0 = disabled
# (HIGH → warn_confirm under the default policy)
# ── Dependency-confusion protection (DC1.1) ───────────────────────────────────
[namespaces]
internal = [] # e.g. ["mycorp-*", "@mycorp/*"] — internal names that
# must NEVER resolve from a public registry (hard BLOCK,
# checked before the allowlist)
# ── Reporting ─────────────────────────────────────────────────────────────────
[reporting]
report_dir = "~/.agentshield/reports/"
auto_report_on_exit = true
# ── License policy (opt-in) ───────────────────────────────────────────────────
[license_policy]
mode = "disabled" # disabled | denylist | allowlist | permissive-only
denied = ["GPL-2.0-only", "GPL-2.0-or-later", "GPL-3.0-only", "GPL-3.0-or-later",
"AGPL-3.0-only", "AGPL-3.0-or-later", "SSPL-1.0", "EUPL-1.1", "OSL-3.0"]
# allowed = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC"]
Response modes
| Mode | CLI identifier | Behaviour |
|---|---|---|
| Block | block |
Refuse install. Returns error to agent. Agent cannot proceed. |
| Warn & confirm | warn_confirm |
Present findings to user. Require explicit approval before allowing. Agent pauses. |
| Async report | async_report |
Allow install unconditionally. Record findings for the next posture report. |
| Ignore | ignore |
Skip this check entirely. No scan overhead. |
Priority resolution
When a finding arrives, AgentShield looks up the response mode in this order (first match wins):
1. rule-level override [rules."T1.2"] mode = "block"
2. ecosystem-level override [ecosystems.pypi] high = "block"
3. global severity default [defaults] high = "warn_confirm"
│
(denylist check: always BLOCK regardless of above)
(namespace check: internal names always BLOCK — even if allowlisted)
(allowlist check: always ALLOW, skips the scan entirely)
API keys
| Key | Where | Effect |
|---|---|---|
NVD_API_KEY |
env var or [api] |
NVD rate limit: 5 req/30s → 50 req/30s. Get one at nvd.nist.gov/developers |
GITHUB_TOKEN |
env var or [api] |
Enables GitHub Advisory Database. Any classic PAT with no scopes works. github.com/settings/tokens |
AgentShield works without either key — OSV has no rate limit and covers most PyPI/npm/Rust packages.
System package scanning ([syspkg])
AgentShield also notices when an agent shells out to a system package manager (apt/apt-get, yum/dnf, apk, brew, snap, pacman, zypper, flatpak, …). This is controlled by the [syspkg] section:
| Key | Default | Effect |
|---|---|---|
enabled |
true |
Detect system installs and emit the lightweight SP1.1 warning. Never blocks. |
cve_scan |
false |
Opt-in. When true, AgentShield runs a live CVE scan (OSV + distro trackers) of the detected packages and applies severity_policy. |
severity_floor |
"HIGH" |
When cve_scan is on, drop findings below this severity so noisy MEDIUM/LOW distro CVEs don't drown the signal. |
max_findings |
50 |
Cap the findings surfaced; any overflow is summarised as "+N more". |
CVE scanning is off by default on purpose: distro packages ship with many low/medium CVEs, so scanning every apt-get install curl would block or nag on routine installs (and slow network paths such as snap install could time out). Detection-and-warn (enabled = true, cve_scan = false) is the default; turn cve_scan on deliberately when you want it. When on, the example policy below blocks CRITICAL and asks for confirmation on HIGH:
[syspkg]
enabled = true
cve_scan = true
severity_floor = "HIGH"
max_findings = 50
[syspkg.severity_policy]
critical = "block"
high = "warn_confirm"
medium = "async_report"
low = "ignore"
info = "ignore"
Offline mode (offline = true at the root of the config, or AGENTSHIELD_OFFLINE=1) skips the CVE scan entirely even when cve_scan = true.
CLI reference
agentshield scan
Scan a single package for vulnerabilities.
agentshield scan <package> [OPTIONS]
Arguments:
package Package name, optionally with version: requests==2.28.0
npm: [email protected] cargo: serde
Options:
-e, --ecosystem [pypi|npm|cargo|rubygems|go] Default: pypi
-c, --config PATH Path to config.toml (default: ~/.config/agentshield/config.toml)
--deep Download wheel and run static analysis (semgrep + bandit + AST)
--offline Local DB only — no network calls
-T, --transitive Resolve and scan transitive dependencies
--transitive-depth INT Maximum resolution depth (default: 3, range 1–10)
--check-licenses Enable license compliance check (denylist mode with defaults)
Output: Rich table of findings with severity, CVSS score, title, source, and remediation hint. Decision (ALLOW / BLOCK / NEEDS_CONFIRMATION / LOG_ASYNC) printed with colour coding.
Exit codes: 0 = allow/warn/log, 1 = block.
Progress indicator: A spinner appears automatically for scans taking > 2 seconds. Deep scans show a distinct "downloading + analyzing" message.
# Examples
agentshield scan requests==2.28.0
agentshield scan [email protected] --ecosystem npm
agentshield scan serde --ecosystem cargo
agentshield scan unknown-pkg --deep
agentshield scan known-pkg --offline
agentshield scan pkg -c /custom/config.toml
agentshield scan flask --transitive # scan flask + all its dependencies
agentshield scan flask -T --transitive-depth 5 # deeper resolution (default: 3)
agentshield scan-file
Scan all packages listed in a manifest file.
agentshield scan-file <path> [OPTIONS]
Arguments:
path Path to a manifest file. Supported formats:
requirements.txt (and variants: test-requirements.txt, dev-requirements.txt, etc.)
pyproject.toml (PEP 621 dependencies + Poetry)
Pipfile.lock
poetry.lock
uv.lock
package.json
package-lock.json
pnpm-lock.yaml (v6 and v9 layouts)
Cargo.toml
Cargo.lock
go.mod
Options:
-c, --config PATH Path to config.toml (default: ~/.config/agentshield/config.toml)
--offline Local DB only — no network calls
--deep Download wheels and run static analysis on each package
-T, --transitive Resolve and scan transitive dependencies
--transitive-depth INT Maximum resolution depth (default: 3, range 1–10)
--check-licenses Enable license compliance check (denylist mode with defaults)
--verify-hashes Verify recorded lockfile hashes against registry digests (H1.x)
Format detection: The format is auto-detected from the filename. If the filename is not a standard name, the file extension is used as a fallback (.txt → requirements, .json → package.json, .toml → Cargo.toml).
Hash verification (--verify-hashes): For manifests that record artifact hashes — pip --hash=sha256:… lines (pip-compile / --emit-hashes layout), Pipfile.lock hash arrays, and package-lock.json SRI integrity strings — each recorded hash is checked against the digests the registry currently publishes for that exact version. A mismatch emits H1.1 (CRITICAL — tampered lockfile or swapped registry artifact; blocks under the default policy); a version the registry no longer serves emits H1.2 (MEDIUM). A registry outage verifies nothing rather than failing the build. Skipped in --offline mode. Also available on the HTTP daemon (/scan-file body field verify_hashes) and the agentshield_scan_file MCP tool.
Output: A Rich summary table showing the status of each package (ALLOW / BLOCK / NEEDS_CONFIRMATION / LOG_ASYNC) with severity and finding count. Overall verdict printed after the table.
Exit codes: 0 = no packages blocked, 1 = one or more packages BLOCKED.
# Examples
agentshield scan-file requirements.txt
agentshield scan-file test-requirements.txt
agentshield scan-file package.json
agentshield scan-file package-lock.json --verify-hashes
agentshield scan-file poetry.lock
agentshield scan-file uv.lock
agentshield scan-file pnpm-lock.yaml
agentshield scan-file go.mod
agentshield scan-file Cargo.toml
agentshield scan-file requirements.txt --check-licenses
agentshield scan-file requirements.txt --deep --transitive
agentshield scan-file /path/to/dev-requirements.txt -c /custom/config.toml
agentshield scan-mcp
Scan MCP server configurations — every stdio MCP server is a package fetched from a registry and executed (npx/uvx/docker), which makes .mcp.json an install manifest in disguise.
agentshield scan-mcp [paths...] [OPTIONS]
Arguments:
paths MCP config files. Omit to auto-discover: .mcp.json, .cursor/mcp.json,
.vscode/mcp.json, ~/.claude.json, claude_desktop_config.json,
~/.codex/config.toml
Options:
-c, --config PATH Path to config.toml
--offline Local DB only
-f, --format terminal|json
The packages each server launches are scanned through the normal pipeline (exit 1 on BLOCK), and risky configuration shapes are flagged:
| Rule | Severity | Meaning |
|---|---|---|
MCP1.2 |
LOW | Launched package is not version-pinned — every launch installs whatever the registry serves |
MCP1.3 |
MEDIUM | Remote server over plain http:// (cleartext credentials/tool traffic) |
MCP1.4 |
INFO | Env forwards secret-looking variables to the server (key names only — values never read) |
MCP1.5 |
HIGH | Launch command can't be statically verified (inline sh -c, shell expansion, VCS URLs) |
Also available as the agentshield_scan_mcp MCP tool.
agentshield sbom
Generate a CycloneDX v1.4 JSON Software Bill of Materials from a manifest file.
agentshield sbom <path> [OPTIONS]
Arguments:
path Path to a manifest file (same formats as scan-file)
Options:
-o, --output PATH Write SBOM to file (default: stdout)
-c, --config PATH Path to config.toml
--offline Local DB only — no network calls
The SBOM includes PURL identifiers for every package and maps any vulnerability findings to their corresponding component entries.
# Examples
agentshield sbom requirements.txt # CycloneDX JSON to stdout
agentshield sbom package.json -o sbom.json # write to file
agentshield sbom Cargo.toml -o sbom.json
agentshield posture
Generate a security posture report for the current environment.
agentshield posture [OPTIONS]
Options:
-f, --format [terminal|json|html|markdown] Output format (default: terminal)
-o, --output PATH Write to file (terminal format ignores this)
-t, --tools TEXT Comma-separated agent tool names to classify
--log-hours INT Hours of async report log to include (default: 24)
--skip-packages Skip installed-package CVE scan (faster, async log only)
-c, --config PATH Path to config.toml
agentshield posture # rich terminal
agentshield posture --format json # JSON to stdout
agentshield posture --format html -o report.html # self-contained HTML file
agentshield posture --format markdown > report.md # Markdown
agentshield posture --tools bash,read_file,web_search # with tool risk classification
agentshield posture --skip-packages # async log only (fast)
agentshield posture --log-hours 72 # last 3 days of async log
agentshield cache
Manage the local scan cache and CVE mirror.
agentshield cache <action> [OPTIONS]
Actions:
stats Show cache statistics (entry counts, CVE mirror size)
clear Delete all cached scan results
warm Download OSV bulk exports and populate local DB
Options: --ecosystems pypi,npm,cargo,rubygems,go (default: all)
Options:
-c, --config PATH
agentshield cache stats
agentshield cache clear
agentshield cache warm
agentshield cache warm --ecosystems pypi,npm
warm downloads OSV advisory bulk exports and populates two tables:
cve_mirror— MEDIUM+ CVEs for fast offline lookupmalicious_packages— packages flaggedtype=MALICIOUSin OSV
agentshield serve
Start the AgentShield daemon.
agentshield serve [OPTIONS]
Options:
--mcp Run as MCP stdio tool server (for any MCP-compatible agent)
--http Run as HTTP REST server on localhost (default port 8765)
--port, -p INT Port for the HTTP server (default: 8765)
--allowed-dirs PATH Comma-separated extra directories allowed for /scan-file and /sbom.
Also accepted via AGENTSHIELD_ALLOWED_DIRS (colon-separated).
--socket PATH Unix socket path (default: ~/.agentshield/agentshield.sock)
-c, --config PATH
agentshield serve # Unix socket JSON-RPC IPC daemon
agentshield serve --mcp # MCP tool server on stdio
agentshield serve --http # HTTP REST server on localhost:8765
agentshield serve --http --port 9000 # custom port
agentshield serve --http --allowed-dirs /ci/workspace # expand path allowlist
agentshield serve --socket /tmp/my.sock # custom IPC socket path
agentshield proxy
Run the scanning index proxy — the manager-agnostic primary enforcement gate. Package managers are pointed at the proxy instead of the public index; every package the resolver asks for (including transitive dependencies) is scanned before it is served. Clean packages are redirected to the real upstream; blocked packages get HTTP 403 so the install fails.
agentshield proxy [OPTIONS]
Options:
--host TEXT Bind address (default: 127.0.0.1)
-p, --port INT Bind port (default: 8799)
--transitive/--no-transitive Also scan resolved transitive deps (default: on)
--print-env Print the export lines and exit
-c, --config PATH
Route managers through it by exporting the env it prints on startup (or agentshield proxy --print-env):
export PIP_INDEX_URL="http://127.0.0.1:8799/simple/" # pip
export UV_INDEX_URL="http://127.0.0.1:8799/simple/" # uv
export npm_config_registry="http://127.0.0.1:8799/npm/" # npm / yarn / pnpm / bun
export GOPROXY="http://127.0.0.1:8799/go" # go (module proxy protocol)
Go coverage: the proxy speaks the go module proxy protocol under /go/ (@v/list, @v/{version}.info|.mod|.zip, @latest, with !-case decoding). Pinned fetches are scanned version-specifically; cleared requests redirect to proxy.golang.org. A blocked module returns 403, which the go command treats as fatal (only 404/410 fall through to another proxy) — deliberately no ,direct fallback, so the gate stays fail-closed.
Not covered: local files, direct wheel/tarball URLs, VCS (git+…) references, and cargo (its source-replacement config is per-project) — those are covered by the PATH shim and execve interceptor layers (agentshield enforce-env prints the full defense-in-depth setup).
agentshield doctor / init
agentshield init # write the recommended starter config
agentshield doctor # verify the installation AND the enforcement wiring
agentshield doctor -f json # machine-readable
doctor answers "is the protection I think I have actually active?" — the failure mode that matters most. Checks: config/DB/audit health, warm-data freshness, API keys, deep-scan tools, shim present and first on PATH, proxy env actually listening, execve preload, Claude Code/Codex hook wiring, the OpenClaw extension, and the Hermes shadow-venv trap (resolves the interpreter behind the hermes wrapper and proves it can import agentshield). Each check reports OK / WARN / FAIL / SKIP with a concrete fix; exit 1 on any FAIL.
Framework integrations
Hermes Agent — pre_tool_call plugin
AgentShield is a real Hermes plugin: a register(ctx) entry point that wires a pre_tool_call hook. The hook fires before every tool runs and scans installs that pass through:
- Shell tools (
terminal— the real Hermes install path — plusbash,shell,run_command,execute,sh) — thecommandargument is parsed forpip/pip3/python -m pip/uv pip,npm/npm i/yarn add, andcargo add/cargo installpatterns. Flags like--break-system-packages,--user,-Uare handled; version specifiers and extras are stripped before scanning. execute_code— the Python body is scanned heuristically, and anyterminal()calls it makes re-enter the hook as realterminalcalls.
Hermes has no
before_tool_call/ToolPlugincontract and no structuredpip_installtool — earliermodule:/class:registration never fired. If you have that anywhere (including in a skill file), remove it.
Install the agentshield[hermes] extra into Hermes's own venv/interpreter (e.g. ~/.hermes/venv) — not a separate environment. This is the single most common mistake: a plugin installed in a different venv than Hermes uses can't be imported by Hermes's loader, so the guard never registers.
# run this against the same interpreter Hermes launches from:
~/.hermes/venv/bin/python -m pip install "agentshield[hermes] @ git+https://github.com/mkarvan/AgentShield.git"
Register — hermes-agent (0.17.x) loads directory plugins only (it does not scan pip entry points), so install the bundled plugin dir and enable it:
cp -r examples/hermes-plugin ~/.hermes/plugins/agentshield
hermes plugins enable agentshield # or add `agentshield` to plugins.enabled in ~/.hermes/config.yaml
Just listing agentshield under plugins.enabled without the ~/.hermes/plugins/agentshield/ directory does nothing (hermes plugins list stays empty). Restart Hermes and confirm with hermes plugins list / /plugins and the log line AgentShield: registered 'pre_tool_call' guard. (pyproject.toml also ships a hermes_agent.plugins entry point for forward-compat with a future entry-point-discovering Hermes; 0.17.x ignores it.)
Decision mapping (pre_tool_call supports allow-or-block only):
| AgentShield decision | Hermes result |
|---|---|
ALLOW / LOG_ASYNC |
Hook returns None — the tool call proceeds |
NEEDS_CONFIRMATION |
{"action": "block", …} with a "needs review" message — fail-closed (Hermes hooks can't prompt) |
BLOCK |
{"action": "block", "message": reason} — agent cannot proceed |
The hook never raises (Hermes swallows hook exceptions and would then run the tool); any internal error becomes a block. A startup self-verify logs loudly if the host lacks the register_hook API.
OpenClaw — before_tool_call plugin (TypeScript)
OpenClaw is a TypeScript/Node framework, so AgentShield ships a Node plugin (under integrations/openclaw/), not a Python module. It registers a before_tool_call hook on the exec tool and blocks unsafe installs by returning OpenClaw's { block: true, blockReason }. Verdicts come from the agentshield CLI (agentshield guard-scan-cmd <command tokens> — exit 1 = block, exit 0 = allow), the same shared scan core used by the Hermes and Claude Code / Codex integrations.
The plugin ships the required openclaw.plugin.json manifest with a configSchema (OpenClaw refuses to load a manifest without one), and its entry point lives in package.json's openclaw.extensions block. OpenClaw also refuses non-root-owned plugin files — install as root (or chown -R root:root the plugin dir).
OpenClaw "skills" are SKILL.md prompt packs, not executable gates, and OpenClaw cannot load a Python class. The earlier Python
AgentShieldSkill(registered viamodule:/class:) was never invoked — it has been removed.
Install (in the OpenClaw box, as root — OpenClaw rejects non-root-owned plugin files; the AgentShield CLI must also be installed, e.g. pipx install agentshield):
openclaw plugins install @agentshield/openclaw-plugin
# or, from a checkout (chown to root first if needed):
sudo chown -R root:root ./integrations/openclaw && openclaw plugins install ./integrations/openclaw
Hook contract (before_tool_call, block: true is terminal):
| AgentShield decision | OpenClaw hook return |
|---|---|
ALLOW / LOG_ASYNC |
undefined (no decision) — the tool call proceeds |
NEEDS_CONFIRMATION |
{ block: true, blockReason } — fail-closed (the hook has no "ask" path) |
BLOCK |
{ block: true, blockReason } — the exec call is vetoed |
The hook never throws (a thrown hook would let the tool run); a missing/erroring sca
No comments yet
Be the first to share your take.