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.
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.
- 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
┌───────────────────────────────── 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 │
└──────────────────────────────────┘ └──────────────────────────────────┘
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
- 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.
Informed by "A Security Analysis of the OpenClaw AI Agent Framework" (arXiv 2603.27517), adapted for supply-chain attack vectors.
| 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 |
| 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 |
| 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 |
| 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 | 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.
pip install git+https://github.com/mkarvan/AgentShield.gitPython 3.11+ required.
Note: PyPI publishing is planned for a future release.
# 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"# 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 --mcpExit codes for agentshield scan: 0 = ALLOW/WARN/LOG_ASYNC, 1 = BLOCK.
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 worksYou can also set them in ~/.config/agentshield/config.toml under [api].
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 an SP1.1 warning, 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 every apt-get install curl or yum install httpd would block or nag on routine installs (and slow paths like snap install could 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.
# ── 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"]| 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. |
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)
| 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.
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.
Scan a single package for vulnerabilities.
agentshield scan <package> [OPTIONS]
Arguments:
package Package name, optionally with version: requests==2.28.0
npm: lodash@4.17.20 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 lodash@4.17.20 --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)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.tomlScan 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.
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.jsonGenerate 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 logManage 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,npmwarm downloads OSV advisory bulk exports and populates two tables:
cve_mirror— MEDIUM+ CVEs for fast offline lookupmalicious_packages— packages flaggedtype=MALICIOUSin OSV
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 pathRun 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 init # write the recommended starter config
agentshield doctor # verify the installation AND the enforcement wiring
agentshield doctor -f json # machine-readabledoctor 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.
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.yamlJust 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 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/openclawHook 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 scanner fails closed for install-looking commands and otherwise lets innocuous commands through. Verify with scripts/openclaw_realtest.sh inside the OpenClaw box.
agentshield serve --mcp starts an MCP-compliant tool server on stdio. Any MCP-compatible framework (Claude, others) connects without a custom adapter — this is the fastest adoption path.
MCP client configuration (e.g., Claude Desktop / Claude Code):
{
"mcpServers": {
"agentshield": {
"command": "agentshield",
"args": ["serve", "--mcp"]
}
}
}Exposed MCP tools:
| Tool name | Description |
|---|---|
agentshield_scan |
Scan a package; returns decision, findings, max severity, trust score |
agentshield_scan_file |
Scan all packages in a manifest file |
agentshield_posture |
Run posture check; returns full JSON report |
agentshield_sbom |
Generate a CycloneDX v1.4 JSON SBOM from a manifest file |
agentshield_diff_scan |
Scan only changed packages between two manifest snapshots |
agentshield_scan_docker |
Scan packages from RUN pip/npm/cargo install in a Dockerfile |
agentshield_scan input schema:
{
"package": "string (required) — package name",
"ecosystem": "pypi | npm | cargo | rubygems | go (required)",
"version": "string (optional) — pinned version",
"deep": "boolean (optional) — default false",
"context_hint": "string (optional) — why the agent wants this package (enables T4.1)",
"transitive": "boolean (optional) — resolve and scan all transitive deps (default false)",
"transitive_depth": "integer (optional) — max resolution depth 1–10 (default 3)",
"check_licenses": "boolean (optional) — check license policy (default false)"
}Response:
{
"decision": "ALLOW | BLOCK | NEEDS_CONFIRMATION | LOG_ASYNC",
"reason": "human-readable explanation",
"max_severity": "NONE | INFO | LOW | MEDIUM | HIGH | CRITICAL",
"trust_score": 87,
"trust_label": "high-trust | moderate | low-trust | suspicious",
"cache_hit": true,
"scan_duration_ms": 42,
"findings": [
{
"rule_id": "CVE-2023-12345",
"title": "Remote code execution in FooLib",
"severity": "CRITICAL",
"cvss_score": 9.8,
"source": "osv",
"remediation": "Upgrade to FooLib >= 2.1.0"
}
],
"transitive_results": []
}agentshield_scan_file input schema:
{
"path": "string (required) — path to manifest file",
"verify_hashes": "boolean (optional, default false) — verify recorded lockfile hashes against registry digests (H1.x)"
}agentshield_sbom input schema:
{
"path": "string (required) — path to manifest file"
}Returns: CycloneDX v1.4 JSON as a text content block.
Without --mcp, agentshield serve starts a Unix domain socket JSON-RPC 2.0 server at ~/.agentshield/agentshield.sock. Useful for shell scripts and Claude Code hooks that need AgentShield without Python startup cost on every call.
Authentication: The IPC server authenticates clients to prevent other local users from accessing the daemon.
- Linux / macOS — peer credential check (SO_PEERCRED / LOCAL_PEERCRED). The server verifies that the connecting process runs as the same UID. No handshake required from the client.
- Other platforms — shared-secret token fallback. A random token is generated at startup and written to
~/.agentshield/ipc.token(mode 0o600). Clients must sendAUTH <token>\nas their first message; the server repliesOK\non success orERR unauthorized\non failure.
Protocol — newline-delimited JSON:
// Request
{"jsonrpc": "2.0", "method": "scan",
"params": {"package": "numpy", "ecosystem": "pypi"}, "id": 1}
// Response
{"jsonrpc": "2.0", "id": 1,
"result": {"decision": "ALLOW", "findings": [], "cache_hit": true}}Available methods: scan, ping.
Shell client example (Linux/macOS — peer credentials, no auth message needed):
echo '{"jsonrpc":"2.0","method":"scan","params":{"package":"requests","ecosystem":"pypi"},"id":1}' \
| nc -U ~/.agentshield/agentshield.sockClaude Code's (and OpenAI Codex's) PreToolUse hook invokes agentshield hook to intercept Bash commands before they run. The hook reads the agent's PreToolUse payload as JSON on stdin, scans every package install in the command through the shared scan core, and replies with the agent's permissionDecision contract on stdout.
// ~/.codex/hooks.json (Codex — needs `codex_hooks = true` under [features] in config.toml)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "agentshield hook --agent codex",
"statusMessage": "AgentShield: scanning install"
}
]
}
]
}
}Codex's hooks.json has no per-hook name field, so it auto-labels unnamed handlers hook1, hook2, …; the statusMessage above is the schema-supported way to make the hook identifiable in the TUI (don't add a "name" key — Codex rejects it).
Decision mapping: BLOCK → permissionDecision: "deny"; warn → "ask" on Claude Code (the user is prompted) / "deny" on Codex (which does not yet honor "ask", so it fails closed); clean → exit 0 with no output (the command proceeds). Anything that can't be verified (shell expansion, VCS URLs, remote requirements files, untrusted conda channels, scanner errors) fails closed and is denied. See AGENT_SETUP.md for the full setup and the Codex enforcement-scope caveats.
agentshield posture generates a point-in-time security assessment of your agent environment.
| Section | Coverage |
|---|---|
| Installed packages | Enumerates packages visible to importlib.metadata; checks each against local CVE mirror and malicious-package DB |
| Risk score | 0–100 score using tanh saturation formula (see below) |
| Critical/high findings | Package-level CVEs and malicious-package flags |
| Attack surface | Agent tools classified as high/medium/low risk |
| Sensitive env vars | Env vars matching *_KEY, *_TOKEN, *_SECRET, etc. (pattern match only — values never read) |
| Async report log | Packages installed under async_report mode within the reporting window |
from math import tanh
score = (
40 * tanh(critical_count / 1.5) # saturates near 40 around 3–4 criticals
+ 25 * tanh(high_count / 2.0) # saturates near 25 around 4–5 highs
+ 20 * tanh(medium_count / 4.0) # saturates near 20 around 8–10 mediums
+ 10 * tanh(low_count / 8.0) # saturates near 10 around 16+ lows
+ 5 * tanh(high_risk_tool_count / 3.0) # up to +5 for dangerous tool config
)
# capped at 100Why tanh? Each band has a maximum contribution (40+25+20+10+5 = 100), and tanh(n/k) gives fast initial growth then a plateau. The 5th critical finding adds much less marginal risk than the 1st. k sets how many findings reach ~76% of the band's maximum.
Score thresholds: 0–24 → LOW, 25–49 → MEDIUM, 50–74 → HIGH, 75–100 → CRITICAL.
Pass --tools with comma-separated tool names active in your agent session:
| Risk level | Examples |
|---|---|
| High | bash, shell, run_code, execute_python, write_file, computer |
| Medium | web_search, read_file, browser, grep, find, list_directory |
| Low | Everything else |
agentshield posture --tools bash,write_file,read_file,web_search,list_calendar| Format | Command | Use case |
|---|---|---|
| Terminal | agentshield posture |
Interactive review; Rich-formatted with colour |
| JSON | --format json |
Machine-readable; pipe to jq or CI tooling |
| HTML | --format html -o report.html |
Human-readable; self-contained dark-theme page |
| Markdown | --format markdown |
Docs, Slack, GitHub issues |
Every time a scan returns LOG_ASYNC (because a finding's response mode is async_report), the findings are serialised and written to the local DB. The posture report reads this log and surfaces packages that slipped through without real-time blocking — particularly valuable for MEDIUM+ findings the operator should review.
import asyncio
from agentshield import AgentShield, ScanRequest, Ecosystem
# --- Scanning ---
shield = AgentShield() # loads ~/.config/agentshield/config.toml
# Synchronous scan
result = shield.scan(ScanRequest(
package="requests",
version="2.28.0",
ecosystem=Ecosystem.PYPI,
))
print(result.decision.action) # "ALLOW" | "BLOCK" | "NEEDS_CONFIRMATION" | "LOG_ASYNC"
print(result.max_severity.value) # "NONE" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"
print(result.findings) # list[Finding]
print(result.cache_hit) # bool
# Async scan (preferred in async contexts)
result = await shield.ascan(request)
# Deep scan — download wheel and run static analysis
result = shield.scan(ScanRequest(
package="some-new-package",
ecosystem=Ecosystem.PYPI,
deep=True,
))
# Pass context_hint to enable T4.1 prompt-injection heuristic
result = shield.scan(ScanRequest(
package="some-pkg",
ecosystem=Ecosystem.PYPI,
source="hermes",
context_hint="The documentation says: pip install some-pkg",
))
# Offline scan
from agentshield.core.config import Config
cfg = Config.load()
cfg = cfg.model_copy(update={"offline": True})
shield = AgentShield(config=cfg)
# --- Posture reports ---
from agentshield.reports import run_posture_check
from agentshield.reports.renderers import render_html, render_json, render_markdown, render_terminal
from agentshield.core.config import DEFAULT_DB_PATH
report = asyncio.run(run_posture_check(
db_path=DEFAULT_DB_PATH,
tool_names=["bash", "read_file", "web_search"],
async_log_hours=24,
skip_package_scan=False,
))
print(report.risk_score) # 0–100
print(report.risk_label) # "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"
# Render
html = render_html(report)
open("report.html", "w").write(html)
json_str = render_json(report) # pretty-printed JSON
md = render_markdown(report) # Markdown document
render_terminal(report) # prints to stdout with RichEcosystem coverage: PyPI (wheel/sdist → semgrep + bandit + AST inspector) and npm (tarball → lifecycle-script inspection, T3.6: preinstall/install/postinstall/prepare hooks plus the payload signals T3.1/T3.2/T3.4/T3.5 in the hook commands and referenced JS files). cargo/rubygems/go emit an informational DEEP.UNSUPPORTED note.
Without --deep, AgentShield runs CVE database lookups and typosquatting checks only — typically < 3 seconds. --deep additionally downloads the package wheel, extracts it to a temp directory, and runs the full analyzer suite.
Deep scanning is implemented for PyPI only. For npm/cargo/rubygems/go,
--deepemits an informationalDEEP.UNSUPPORTEDfinding and the online CVE/advisory checks still apply — registry artifacts for those ecosystems lack the lockfiles the audit tooling needs.
Use --deep for:
- New packages from unknown authors
- Any package an agent is requesting for the first time
- Interactive scans where latency is acceptable (target P95: < 15 seconds)
Analyzers:
| Analyzer | Language | Tool | Graceful if absent |
|---|---|---|---|
setup_py_inspector |
Python | stdlib ast |
N/A (always runs) |
semgrep_runner |
Python | semgrep CLI |
Yes — skips, emits DEBUG log |
bandit_runner |
Python | bandit CLI |
Yes — skips, emits DEBUG log |
(npm_audit_runner and cargo_audit_runner exist in the tree but are not wired into the scan pipeline — deep scanning is PyPI-only; see the note above.)
Custom semgrep rules ship in src/agentshield/analyzers/rules/:
| Rule file | Threat | Detects |
|---|---|---|
T3_1_shell_exec.yaml |
T3.1 | subprocess, os.system, eval, exec in install scripts |
T3_2_network_install.yaml |
T3.2 | urllib.request, requests, socket calls in install scripts |
T3_3_filesystem_write.yaml |
T3.3 | open(path, "w"), shutil.copy to sensitive directories |
T3_4_obfuscation.yaml |
T3.4 | exec(base64.b64decode(...)), marshal/zlib deobfuscation |
T3_5_credential_harvest.yaml |
T3.5 | os.environ.get("*_TOKEN"), os.environ["*_KEY"] |
See also:
docs/deep-mode.mddocuments the supply chain risks inherent in downloading packages for analysis and the recommended mitigations.
Pass -T / --transitive to resolve and scan the full dependency tree of a package, not just the package itself.
agentshield scan flask --transitive # scan flask + all transitive deps
agentshield scan django==4.2.0 -T --transitive-depth 5 # resolve up to 5 levels deep--transitive-depth controls the maximum resolution depth (default: 3). Transitive results are printed in a separate summary table after the direct-package findings.
A blocked transitive dependency fails the scan: agentshield scan --transitive exits 1 even when the requested package itself is clean, and manifest scans (scan-file, sbom, scan-docker) count such packages as blocked in the aggregate decision — installing the parent would install the blocked dependency.
Transitive scanning uses the registry API (PyPI JSON, npm registry, crates.io) to resolve dependency metadata without downloading or installing anything. Note: dependency resolution requires network access, so it is skipped entirely in --offline mode (the offline promise — no network — takes precedence). Transitive dependencies are scanned with the standard (non-deep) pipeline; --deep applies to the requested package only.
agentshield sbom and the agentshield_sbom MCP tool generate a CycloneDX v1.4 JSON Software Bill of Materials from a manifest file. The output includes:
- PURL identifiers for every listed package
- Vulnerability findings mapped to their corresponding component entries
agentshield sbom requirements.txt # CycloneDX JSON to stdout
agentshield sbom package.json -o sbom.json # write to fileVia MCP:
{ "tool": "agentshield_sbom", "path": "requirements.txt" }AgentShield can check package licenses against a configurable policy before allowing installation. This is opt-in — add a [license_policy] section to config.toml to enable.
[license_policy]
mode = "denylist" # disabled | denylist | allowlist | permissive-only
# Packages whose license matches any entry in `denied` are flagged.
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",
]
# Used in allowlist mode: only these licenses are allowed.
# allowed = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC"]| Mode | Behaviour |
|---|---|
disabled |
No license checking (default) |
denylist |
Flag packages whose license appears in denied |
allowlist |
Flag packages whose license is NOT in allowed |
permissive-only |
Flag any copyleft or known non-permissive license |
| License family | Severity |
|---|---|
| GPL-2/3, AGPL-3, SSPL | CRITICAL |
| LGPL-2/3, EUPL, OSL, MPL | HIGH |
| Other denied/non-allowlisted | MEDIUM |
# One-off check using default denylist (no config.toml change needed):
agentshield scan requests --check-licenses
# With ecosystem:
agentshield scan lodash --ecosystem npm --check-licensesThe --check-licenses flag activates denylist mode with default denied licenses even if config.toml has mode = "disabled". If a non-disabled mode is already configured, the flag is a no-op.
Pass "check_licenses": true to agentshield_scan:
{
"package": "requests",
"ecosystem": "pypi",
"check_licenses": true
}| Ecosystem | Source |
|---|---|
| PyPI | Trove classifiers (primary), info.license field (fallback) |
| npm | license field in registry metadata |
| Cargo | license field in crates.io API (SPDX expression) |
SPDX identifiers are normalised before comparison (GPLv2 → GPL-2.0-only, MIT License → MIT, etc.).
AgentShield ships a pre-commit hook that scans manifest files for vulnerable or malicious packages before every commit.
# 1. Install pre-commit (if not already installed)
pip install pre-commit
# 2. Add to .pre-commit-config.yaml in your repo
cat >> .pre-commit-config.yaml <<'EOF'
repos:
- repo: https://github.com/mkarvan/AgentShield
rev: v0.14.0
hooks:
- id: agentshield-scan
EOF
# 3. Install hooks
pre-commit install
# 4. Test
pre-commit run agentshield-scan --all-filesThe hook triggers on: requirements*.txt, Pipfile.lock, package.json, package-lock.json, Cargo.toml, Cargo.lock, pyproject.toml.
Exit codes: 0 = all packages allowed, 1 = one or more blocked (commit aborted).
See docs/pre-commit.md for advanced configuration (custom config path, offline mode, allowlist setup).
agentshield-action is a composite GitHub Action that scans manifest files in pull requests and posts a markdown security report as a PR comment.
# .github/workflows/security-scan.yml
name: Security Scan
on:
pull_request:
paths: ["**/requirements*.txt", "**/package*.json", "**/Cargo.toml"]
jobs:
agentshield:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: ./.github/action/agentshield-action
with:
check-licenses: "true"
fail-on: HIGHInputs: manifests (glob pattern), check-licenses (bool), fail-on (severity threshold), deep (bool), transitive (bool), github-token, version (AgentShield version to install, default 0.14.0).
Not on PyPI yet. AgentShield is not published to PyPI or the GitHub Actions Marketplace. The action installs from source (
pip install "git+https://github.com/mkarvan/AgentShield.git@v<version>") and is referenced by local path (./.github/action/agentshield-action), soversionmust be a valid Git tag. Likewise, install the CLI withpip install git+https://github.com/mkarvan/AgentShield.git— a barepip install agentshieldwill not resolve until a PyPI release is published.
Outputs: blocked, warned, total, report (markdown text).
The PR comment is updated in-place on re-runs — no duplicate comments. See docs/github-action.md for full documentation and examples.
AgentShield records every scan decision in a local scan_history table. On each subsequent scan of the same package, the result is compared against the last recorded decision:
| Transition | Finding | Severity |
|---|---|---|
| ALLOW → BLOCK | D1.1 | HIGH |
| ALLOW → WARN / LOG_ASYNC | D1.1 | MEDIUM |
When drift is detected, a D1.1 Finding is added to the current scan result and surfaced in the response. The posture report also includes a Drift Detected section.
# Re-scan all previously-allowed packages and report any regressions
agentshield drift-check
# JSON output
agentshield drift-check --format jsonExit code: 0 = no drift, 1 = one or more packages have regressed.
AgentShield tracks per-session package installation rates and cumulative wheel download size. Limits are enforced before online checks run — if a limit is exceeded, the scan returns a BLOCK decision with a R1.1 Finding (severity HIGH) immediately. Cumulative wheel size is accrued from --deep scans (which download the artifact) and each download is also capped at max_wheel_mb_per_session individually; a session already over budget is blocked on its next scan.
[rate_limits]
max_packages_per_hour = 20 # max package scans per 1-hour window (default 20)
max_wheel_mb_per_session = 500 # max wheel download MB across the entire session (default 500)Session state is stored in SQLite and identified by the AGENTSHIELD_SESSION_ID env var (auto-generated UUID if unset, persists within a process).
| Variable | Purpose |
|---|---|
AGENTSHIELD_SESSION_ID |
Override the session ID (useful in multi-process setups) |
Scan only the packages that changed between two manifest snapshots — useful in CI to scan just the PR delta rather than the full manifest.
agentshield diff-scan old-requirements.txt new-requirements.txt
agentshield diff-scan old-package.json new-package.jsonEach package is classified as:
| Category | Scanned? | Description |
|---|---|---|
| added | Yes | Present in new manifest, absent in old |
| upgraded | Yes | Same package, different version |
| removed | Listed only | Present in old, absent in new |
| unchanged | Skipped | Same package + version in both |
The command exits with code 1 if any added or upgraded package is blocked.
MCP tool: agentshield_diff_scan with old_path and new_path arguments.
Every scan now includes a 0–100 trust score reflecting a package's reputation based on publicly available signals.
| Score | Label | Finding |
|---|---|---|
| 80–100 | high-trust | None |
| 50–79 | moderate | None |
| 20–49 | low-trust | T5.1 HIGH |
| 0–19 | suspicious | T5.1 CRITICAL |
- PyPI: package age (earliest release upload date), release count, metadata completeness (homepage, summary, author), monthly download count via pypistats.org
- npm: creation date, version count, maintainer count, monthly download count via npm downloads API
- crates.io: creation date, version count, total download count
- Scan history: past BLOCK decisions in local AgentShield DB penalise the score
Trust score computation runs concurrently with other checks and never blocks or delays a scan result — if the registry is unreachable the score is omitted (null).
The score is included in all output formats: terminal (Trust: 87/100 (high-trust)), JSON ("trust_score": 87), MCP tool results, and HTTP API responses.
Scan packages referenced in a Dockerfile's RUN install commands:
agentshield scan-docker Dockerfile
agentshield scan-docker path/to/DockerfileSupported patterns inside RUN instructions:
| Pattern | Ecosystem |
|---|---|
pip install …, pip3 install …, python -m pip install …, uv pip install … |
PyPI |
npm install …, npm i …, yarn add … |
npm |
cargo add …, cargo install … |
Cargo |
gem install … |
RubyGems |
go install …, go get … |
Go |
Both shell form (RUN pip install foo) and exec form (RUN ["pip", "install", "foo"]) are supported. Backslash-newline continuations are handled. Duplicate packages across multiple RUN instructions are deduplicated. Exact version pins (pip install requests==2.19.0, npm install lodash@4.17.20) are scanned at the pinned version; range specifiers and dist-tags fall back to latest.
MCP tool: agentshield_scan_docker with a path argument.
Run AgentShield as an HTTP REST API server on localhost:
agentshield serve --http # default port 8765
agentshield serve --http --port 9000
# Allow extra directories for CI/CD (comma-separated)
agentshield serve --http --allowed-dirs /ci/workspace,/builds
# Or via environment variable (colon-separated)
AGENTSHIELD_ALLOWED_DIRS=/ci/workspace:/builds agentshield serve --http| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness probe — returns {"status": "ok"} |
POST |
/scan |
Scan a single package (same request body as MCP agentshield_scan) |
POST |
/scan-file |
Scan a manifest file — {"path": "/abs/path/to/requirements.txt"} |
GET |
/posture |
Generate a security posture report |
POST |
/sbom |
Generate a CycloneDX SBOM — {"path": "/abs/path/to/manifest"} |
All responses are JSON. Errors return {"error": "..."} with the appropriate HTTP status code.
/scan-file and /sbom resolve the supplied path and verify it falls inside an allowed directory — anything outside returns 403 Forbidden.
Default allowlist: CWD, home directory, /tmp, and the system temp directory (e.g. $TMPDIR on macOS). This covers the common CI/CD pattern of writing manifests to /tmp before calling the HTTP API.
Expanding the allowlist:
| Method | Syntax |
|---|---|
| CLI flag | --allowed-dirs /path/a,/path/b (comma-separated) |
| Env var | AGENTSHIELD_ALLOWED_DIRS=/path/a:/path/b (colon-separated) |
| Python API | Pass allowed_dirs=[Path("/path/a")] to the HTTPServer constructor |
The HTTP server uses Python's asyncio stdlib — no extra dependencies required. It binds to 127.0.0.1 and has no authentication, so it must not be exposed to untrusted networks (binding to a non-loopback host logs a warning). Requests whose Host header is not a loopback name (or the configured bind host) are rejected with 403 as a DNS-rebinding guard. Request bodies larger than 10 MB are rejected with 413 Payload Too Large.
Start an interactive shell session where pip, npm, and cargo install commands are intercepted before execution — and the run-without-install forms too: npx, uvx, bunx, pipx run, npm exec/x, pnpm/yarn dlx, bun x, uv tool run all fetch a registry package and execute it in one step, so they are scanned like installs (only the executed package is checked; the tool's own arguments are left alone). System package managers (apt-get, yum, dnf, brew, apk, pacman, zypper, pkg, emerge, snap, flatpak) are also detected and flagged with an SP1.1 warning. Live CVE scanning of those system packages is opt-in ([syspkg].cve_scan, off by default) — see the callout below:
agentshield guard # wraps $SHELL (bash, zsh, or fish)
agentshield guard --shell zsh # wrap a specific shellInside the guarded shell, the package manager commands are shadowed by wrapper functions. Any pip install, npm install, or cargo add/cargo install calls agentshield guard-scan-cmd first — if AgentShield blocks the package, the install is aborted and the error is printed before the command runs. Packages declared in a referenced requirements/constraint file (pip install -r requirements.txt) are resolved and scanned as well; a remote -r <url> is blocked because its contents cannot be verified.
System package managers are also intercepted: apt-get install, brew install, yum install, dnf install, apk add, pacman -S, zypper install, snap install, flatpak install, pkg install, and emerge all trigger an SP1.1 warning. They are not CVE-scanned unless you opt in.
Note
syspkg.cve_scan defaults to false. Out of the box, system installs are detected and warned about (SP1.1) but not blocked — everyday installs proceed without a CVE scan. Set cve_scan = true under [syspkg] to additionally scan them via OSV and distro-specific trackers (Ubuntu CVE, Red Hat CVE, Homebrew audit). When enabled, the configurable severity policy applies — the recommended policy blocks critical CVEs, asks for confirmation on high, and logs medium asynchronously — filtered by severity_floor (default HIGH) and capped at max_findings (default 50). See System package scanning ([syspkg]).
[syspkg]
cve_scan = true # default: false — opt in to scan system packages
severity_floor = "HIGH" # ignore MEDIUM/LOW distro CVEs
max_findings = 50 # cap findings; overflow shown as "+N more"
[syspkg.severity_policy]
critical = "block"
high = "warn_confirm"[AgentShield Guard] Active — pip, npm, and cargo install commands are protected.
[guard] $ pip install evil-pkg
AgentShield BLOCKED 1 package(s):
• evil-pkg: malicious package detected
[guard] $ ← install was aborted
Supported shells: bash, zsh, fish (defaults to bash-compatible for others).
Exit the guarded shell normally (exit or Ctrl-D) to return to your regular session.
Three ways to activate:
agentshield scan pkg --offline # CLI flag
AGENTSHIELD_OFFLINE=1 agentshield scan pkg # environment variable
# Or in config.toml: offline = trueOffline scans query only:
cve_mirrortable (populate withagentshield cache warm)malicious_packagestable- In-process typosquatting checker (no network)
When the scan pins a version (pkg==1.2.3), mirrored advisories are filtered
against their stored affected-version ranges: advisories that confidently do
not affect the pinned version are dropped. Unpinned scans and advisories whose
ranges cannot be parsed are still reported (fail toward reporting).
Offline verdicts are only as good as the local mirror. Offline scans warn on
stderr when the warmed data is missing or older than warm_max_age_days
(default 7), and agentshield cache stats shows the per-ecosystem warm age.
Long-running daemons (agentshield serve, any mode) can keep the mirror fresh
automatically with auto_warm = true under [cache] — stale ecosystems are
re-downloaded in the background every auto_warm_interval_hours (default 24).
Target latency: < 50 ms. Static analysis (--deep) is unavailable offline.
All scan results are cached in a local SQLite database with severity-based TTLs:
| Max severity | Cache TTL |
|---|---|
| NONE (clean) | 7 days |
| INFO | 24 hours |
| LOW | 12 hours |
| MEDIUM | 6 hours |
| HIGH | 6 hours |
| CRITICAL | 3 hours |
The cache also stores a CVE mirror (populated by agentshield cache warm) and the async report log. All data is stored in ~/.agentshield/agentshield.db by default.
agentshield cache stats # show entry counts
agentshield cache clear # delete all cached scan results
agentshield cache warm # (re-)populate CVE mirror and malicious DBAgentShield checks at startup that the binary you're running matches the version installed in the current Python environment. If you see:
[agentshield] Warning: running v0.12.1 but v0.13.0 is installed. Your PATH may be pointing to a stale binary.
This means the agentshield binary found in your PATH comes from a different Python environment than the one that has the newer version installed. Common causes:
- A global install (
pip install --user agentshield) shadows a venv install - The shell session predates
pip install(the old binary is still cached inPATH)
Fix:
# 1. Make sure your venv is activated
source .venv/bin/activate # or: conda activate myenv
# 2. Verify which binary is being run
which agentshield
# 3. If it still points to the wrong location, reinstall into the active env
pip install --force-reinstall agentshieldEvery scan decision — including allowlist/denylist short-circuits, cache hits, and rate-limit blocks — is appended to a local, hash-chained audit log (default: ~/.agentshield/audit/audit-YYYY-MM.jsonl). Each JSONL record carries prev_hash and hash = sha256(prev_hash + record), so deleting, editing, or reordering records breaks the chain detectably. Files rotate monthly and by size.
agentshield audit tail # most recent decisions (table)
agentshield audit tail -n 50
agentshield audit tail --format json # one JSON record per line (grep/jq/CI)
agentshield audit verify # walk every file's hash chain; exit 1 on tampering[audit]
enabled = true # default on; local only, no telemetry
dir = "~/.agentshield/audit"
max_mb = 50 # per-file size before rotation
retention_days = 365 # prune files wholly older than this; 0 = keep foreverEnvironment overrides: AGENTSHIELD_AUDIT=0 disables, AGENTSHIELD_AUDIT_DIR relocates the directory. Audit writes are best-effort — a failed write logs a warning and never fails the scan.
Query index. audit tail and the posture report's audit summary are served from a derived SQLite index (audit_index.sqlite in the audit directory) that syncs incrementally — only lines appended since the last query are parsed, so queries stay fast as the logs grow. The JSONL files remain the source of truth: the index is disposable (delete it and it rebuilds), and every reader falls back to walking the files directly if the index can't be used.
Retention. Files whose entire month is older than retention_days are pruned automatically on rotation (or on demand with agentshield audit prune). Set retention_days = 0 to keep everything.
Forwarding to a log server. The local files stay the source of truth; new records can additionally be shipped to an HTTP collector (NDJSON batches — Vector, Fluent Bit, custom receivers) and/or a syslog server (RFC 5424 over UDP/TCP):
[audit.forward]
url = "https://collector.example/ingest" # HTTP: application/x-ndjson batches
# syslog_host = "siem.example" # and/or RFC 5424 syslog
# syslog_port = 514
# syslog_protocol = "udp" # udp | tcp
batch_size = 500
interval_seconds = 60 # daemon flush cadenceDelivery is cursor-tracked (at-least-once): the cursor only advances after a batch is accepted by every configured target, so records are never lost — an interrupted pass resumes exactly where it stopped. agentshield serve daemons flush in the background every interval_seconds; one-shot environments use agentshield audit forward (cron/CI-friendly). Records carry their chain hashes, so receivers can verify integrity independently.
Cryptographic verification (opt-in): with pip install "agentshield[attestations]" and [provenance] verify = true, PyPI (PEP 740) attestations are verified against the artifact's registry sha256 and the publisher identity claimed in the bundle. A present-but-invalid attestation emits T6.4 (HIGH) — a stronger tamper signal than absence. Without the extra, checks stay presence-only (annotated as unverified).
AgentShield can check whether the exact artifact a scan targets carries a registry provenance attestation — PyPI PEP 740 attestations (Trusted Publishers) and npm build/publish provenance. Other ecosystems are not yet checked and are never penalised: RubyGems.org now hosts Sigstore attestations and crates.io supports Trusted Publishing (authentication only — no consumer-verifiable attestations yet), while the Go ecosystem provides checksum transparency rather than publisher attestations. RubyGems attestation checks are a candidate for a future release.
[provenance]
mode = "off" # off (default) | log | requirelog— aT6.1INFO finding records a present attestation and its publisher; absence emits nothing (most packages are not yet attested).require— absence on PyPI/npm additionally emitsT6.2(HIGH →warn_confirmunder the default policy; add[rules."T6.2"] mode = "block"to hard-block unattested packages).
Regression tracking (T6.3). Every checked version is recorded locally; when a package that previously published attestations releases a version without one, T6.3 (HIGH) fires in both log and require modes — the attested→unattested transition is the actual compromise signal (a hijacked publishing pipeline usually can't reproduce the maintainer's Trusted Publisher identity).
Registry errors emit no findings — an outage must not block installs (CVE checks remain the security floor).
Artifact hashes: agentshield scan pkg==1.2.3 --emit-hashes prints pip hash-checking lines for the pinned version after a passing scan:
requests==2.32.3 \
--hash=sha256:… \
--hash=sha256:…
scan-file, diff-scan, and scan-docker accept --format sarif (optionally -o results.sarif) to emit SARIF 2.1.0 for GitHub code scanning and other SARIF consumers. Severities map CRITICAL/HIGH → error, MEDIUM → warning, LOW/INFO → note; CVSS scores populate GitHub's security-severity. Manifest lines are located best-effort by searching for the package name.
# GitHub Actions
- run: agentshield scan-file requirements.txt --format sarif -o agentshield.sarif
continue-on-error: true # exit 1 = blocked packages; still upload
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: agentshield.sarifExit codes are unchanged (1 when any package is blocked), so the same invocation still gates CI.
Accept a specific finding for a limited time without abusing allowlists (which never expire and hide everything):
[[waivers]]
rule_id = "CVE-2024-1234" # or "T5.1", "L1.1", "T6.2", …
package = "requests" # optional; omit = any package
ecosystem = "pypi" # optional
expires = 2026-09-30 # REQUIRED — no permanent waivers
reason = "vendor fix pending, tracked in JIRA-123" # REQUIREDWaived findings stay visible in scan output, SARIF, and the audit log (annotated waived, with the reason and expiry) but no longer drive block/warn decisions. Expired waivers are ignored automatically.
agentshield waivers list # all waivers with expiry status
agentshield waivers check # exit 1 if any waiver has expired — add to CI
# Add a waiver from the CLI instead of hand-editing TOML — validates the date
# (must be in the future), the reason (required), rejects duplicate active
# scopes, and checks the resulting config parses before writing:
agentshield waivers add CVE-2024-1234 \
--expires 2026-12-31 --reason "vendor fix pending, tracked in JIRA-123" \
--package requests --ecosystem pypiWaivers apply to findings; denylist blocks are configuration, not findings — edit the denylist instead.
Send BLOCK / drift events to Slack (or any webhook):
[notifications]
webhook_url = "https://hooks.slack.com/services/…"
events = ["block", "drift"] # block | needs_confirmation | log_async | drift
format = "slack" # slack (default; also fits Discord/Mattermost) | json
timeout_seconds = 2.0drift fires whenever a D1.1 finding appears (a previously-allowed package regressed), regardless of the decision. Delivery is best-effort: one bounded POST per matching decision; failures are logged and never change or fail a verdict.
AgentShield has four test tiers, from fastest to most thorough:
| Tier | Command | What it proves | Network |
|---|---|---|---|
| Unit + offline e2e | pytest tests/unit tests/e2e |
Core logic, scan pipeline, parsing, config, servers, and every analyzer, with mocked enrichment (~1 450 tests, ~4 min) | No |
| Integration | pytest tests/integration --no-cov |
The real OSV / NVD / GitHub Advisory APIs still answer the way the clients expect | Yes (live APIs) |
| Real-instance tier (source of truth for enforcement) | AGENTSHIELD_E2E_REAL=1 pytest tests/e2e_real -m "real and not llm" --no-cov |
The full feature matrix against real Hermes / OpenClaw runtimes and real package managers in containers | Builds images; verdicts run offline |
| Model-driven (LLM) tier | AGENTSHIELD_E2E_REAL=1 AGENTSHIELD_LLM_E2E=1 pytest tests/e2e_real/test_llm_real.py -m "real and llm" --no-cov |
A real model, mid-task, attempts bad pip/npm/cargo installs in your own credentialed boxes and AgentShield blocks them (clean install stays allowed) | Yes (model API) |
There's also a smoke harness — sh scripts/container_e2e_test.sh inside
any container with agentshield on PATH — for a quick "is the installed
build alive in this box?" check of the core enforcement surfaces.
# Lint + format + types (the same gate CI runs)
ruff check src tests && ruff format --check src tests && mypy src
# Fast suite: unit + offline e2e (with coverage)
pytest tests/unit tests/e2e
# One module / one test
pytest tests/unit/test_scanner.py -q --no-cov
pytest tests/unit/test_versions.py -k osv_ranges -q --no-cov
# Network integration (live OSV/NVD/GitHub APIs; auto-skips offline)
pytest tests/integration --no-cov
# Real-instance container tier (macOS `container` CLI, docker, or podman —
# auto-detected; on macOS start the service first: `container system start`)
AGENTSHIELD_E2E_REAL=1 pytest tests/e2e_real -m "real and not llm" --no-cov
# ...one runtime only, or with pinned upstream versions
AGENTSHIELD_E2E_REAL=1 pytest tests/e2e_real/test_hermes_real.py --no-cov
AGENTSHIELD_E2E_REAL=1 HERMES_VERSION=0.17.2 OPENCLAW_VERSION=1.4.0 \
pytest tests/e2e_real -m "real and not llm" --no-cov
# Model-driven (LLM) tier against your OWN running, credentialed boxes
AGENTSHIELD_E2E_REAL=1 AGENTSHIELD_LLM_E2E=1 \
HERMES_LLM_CONTAINER=<your-hermes-box> OPENCLAW_LLM_CONTAINER=<your-openclaw-box> \
pytest tests/e2e_real/test_llm_real.py -m "real and llm" --no-cov -v
# ...narrow the LLM matrix or shorten the per-scenario budget
AGENTSHIELD_LLM_SCENARIOS=bad_pip,clean_allow LLM_TIMEOUT=120 \
AGENTSHIELD_E2E_REAL=1 AGENTSHIELD_LLM_E2E=1 \
HERMES_LLM_CONTAINER=<box> OPENCLAW_LLM_CONTAINER=<box> \
pytest tests/e2e_real/test_llm_real.py -m "real and llm" --no-covWithout AGENTSHIELD_E2E_REAL=1 (or without a container engine) the whole
real tier skips cleanly, so pytest tests is always safe to run.
This is the authoritative test of the runtime enforcement layers. It builds
containers with real hermes-agent and OpenClaw, a local PyPI/npm index,
and AgentShield installed per AGENT_SETUP.md, then drives the
runtimes' own plugin loaders and enforcement paths — catching the "plugin
loaded but the hook never fired" class of failure that no offline test can.
Works with the macOS container CLI as well as docker/podman (auto-detected).
# macOS: start the container service first
container system start
AGENTSHIELD_E2E_REAL=1 pytest tests/e2e_real -m real --no-covIt covers: real Hermes interception, real OpenClaw interception, Claude Code /
Codex hook goldens, real pip install through the PATH shim (blocked package
is verified not importable afterwards), the index proxy, the audit chain +
tamper + forward pipeline, and the full feature matrix — warn_confirm,
cache-key isolation, RubyGems/Go, waivers, SARIF, conda trust, boolean flags,
the execve interceptor, drift + notifications, posture + SBOM, and hook edge
cases. A separate model-driven (LLM) tier
(tests/e2e_real/test_llm_real.py) attaches to your own credentialed boxes
to prove a real agent mid-task gets intercepted — see
tests/e2e_real/README.md.
A ~270-line self-contained POSIX-sh (BusyBox/ash-safe) script for a fast check
inside any container that has agentshield on PATH. It seeds a known-bad
sentinel into the local malicious_packages table (tagged, removed on exit via
a trap), and grades the core surfaces — guard-scan-cmd, PATH shim, execve
interceptor, Claude Code/Codex hook — with a BAD (expect BLOCK) and GOOD
(expect ALLOW) package. It is not the full matrix; it points you at the
real-instance tier for that. See scripts/README.md for
provisioning (container_install_deps.sh) and the -i-over-stdin invocation.
AgentShield targets Python 3.11+ (CI runs 3.11 / 3.12 / 3.13). Set up an editable install with the dev extras:
git clone https://github.com/mkarvan/AgentShield
cd AgentShield
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# add static-analysis if you'll work on the --deep code paths:
# pip install -e ".[dev,static-analysis]"These mirror .github/workflows/ci.yml exactly. All of them are expected to
pass locally before you push (the same jobs gate main and every PR):
# Lint & type-check (the "lint" job)
ruff check src/ tests/
ruff format --check src/ tests/
mypy src/agentshield # mypy runs in --strict mode (see pyproject.toml)
# Unit tests with coverage (the "test" job; coverage must stay ≥ 60%)
pytest tests/unit/ -v --cov=agentshield --cov-report=term-missing \
--cov-fail-under=60 -m "not integration"The CI lint job also installs types-toml alongside the dev extras. Ruff is
configured with line-length = 100, target-version = py311, and the
E, F, I, UP, B, SIM rule sets (see [tool.ruff] in pyproject.toml).
Integration tests hit real advisory APIs (OSV / NVD / GitHub Advisory) and only
run in CI on pushes to main; run them locally with API keys when touching the
online scanning paths:
NVD_API_KEY=... GITHUB_TOKEN=ghp_... pytest tests/integration/ -v -m integrationMatch your change to the right tier so the next contributor's tests stay meaningful:
| You changed… | Add/update tests in… | Then run |
|---|---|---|
| Core logic, an analyzer, a DB client, config, models | tests/unit/ (mock enrichment with respx) |
pytest tests/unit -m "not integration" |
| The scan pipeline / CLI surface end-to-end | tests/e2e/ |
pytest tests/e2e -m "not network" |
An enforcement surface — Hermes plugin, OpenClaw Node plugin, Claude Code/Codex hook, guard-scan-cmd, PATH shim, LD_PRELOAD execve, index proxy |
a section in tests/e2e_real/inside/* + a case in the matching tests/e2e_real/test_*.py |
AGENTSHIELD_E2E_REAL=1 pytest tests/e2e_real -m real --no-cov |
| A new user-facing feature (a new rule family, output format, policy knob, notification target, …) | a new section in tests/e2e_real/inside/feature_checks.sh and a matching entry in _SECTIONS in tests/e2e_real/test_matrix_features.py |
the same real-instance command above |
Anything in AGENT_SETUP.md (install/enable steps) |
it's exercised by the image builds — rebuild the tests/e2e_real images |
the same real-instance command above |
The real-instance tier (tests/e2e_real/) is the source of truth for
enforcement and feature behavior; scripts/container_e2e_test.sh is only a fast
smoke check. Keep the coverage tables in tests/e2e_real/README.md current when
you add a section.
Before submitting a change to any enforcement surface, run the real-instance
tier and confirm a clean run. For the two framework plugins there are also
standalone ad-hoc scripts you can run inside a live box you already have:
scripts/hermes_realtest.sh (set HERMES_PY to Hermes's interpreter) and
scripts/openclaw_realtest.sh — both FAIL if the hook never fires. These
are the same checks the tests/e2e_real Hermes/OpenClaw tests run in CI.
| Directory | What's tested | Network needed |
|---|---|---|
tests/unit/ |
Core logic, cache, config, models, response engine, static analysis rules, posture scoring, renderers, IPC auth, SBOM, manifest parsers | No |
tests/integration/ |
Real API calls (OSV, NVD, GitHub Advisory) | Yes |
tests/e2e/ |
Full scan pipeline including IPC socket server + enforcement matrix (offline) | No |
tests/e2e_real/ |
Real Hermes/OpenClaw runtimes + real package managers in containers; full feature matrix; optional model-driven (LLM) tier | Builds containers |
tests/fixtures/packages/ contains synthetic packages that trigger specific rules:
| Directory | Triggers |
|---|---|
shell_exec/ |
T3.1 |
network_at_install/ |
T3.2 |
filesystem_write/ |
T3.3 |
obfuscated_payload/ |
T3.4 |
cred_harvester/ |
T3.5 |
benign_package/ |
None (false-positive baseline) |
| Variable | Purpose |
|---|---|
NVD_API_KEY |
NVD API key (higher rate limit; enables NVD integration tests) |
GITHUB_TOKEN |
GitHub PAT (enables Advisory DB; integration tests) |
AGENTSHIELD_OFFLINE |
1 to force offline mode without editing config |
