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.

Python 3.11+ License: MIT v0.14.0

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 requets instead of requests, or panda instead of pandas
  • 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

┌───────────────────────────────── 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. --deep downloads 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 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: false

See 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 lookup
  • malicious_packages — packages flagged type=MALICIOUS in 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 — plus bash, shell, run_command, execute, sh) — the command argument is parsed for pip/pip3/python -m pip/uv pip, npm/npm i/yarn add, and cargo add/cargo install patterns. Flags like --break-system-packages, --user, -U are handled; version specifiers and extras are stripped before scanning.
  • execute_code — the Python body is scanned heuristically, and any terminal() calls it makes re-enter the hook as real terminal calls.

Hermes has no before_tool_call/ToolPlugin contract and no structured pip_install tool — earlier module:/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 via module:/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