New in v0.7.0 — released Wednesday, 2026-05-20

v0.7 is the release that closes the MCP-bypass gap and turns Shield into a policy tuner you can run against your own logs.

  • aperion-shield --install-hooks (new headline): writes a managed Git pre-commit and pre-push hook into .git/hooks/. Same engine, same shieldset.yaml, same severity tiers — now enforced on every git commit and git push even when the agent reaches around MCP via a direct shell. Idempotent (re-runnable), coexists with husky / pre-commit / lefthook via --chain-existing, honours both git --no-verify and SHIELD_HOOKS_DISABLE=1 for the rare legitimate bypass. Full section ↓
  • aperion-shield --suggest-rules (new): reads your local shield_eval JSON-Lines audit log and emits text / markdown / yaml-patch suggestions across three classes — rules that never fire, rules that get consistently demoted by decision memory, and noisy Warn rules that should probably be downgraded. --suggest-format yaml-patch produces splice-ready snippets for shieldset.yaml with rationale comments. Full section ↓
  • Four new IDE quickstarts: Cline, Continue, Windsurf, Zed (joining Cursor + Claude Code). Schema differences are documented per-IDE in the README. Zed in particular uses context_servers (not mcpServers) with a nested command:{path,args} shape.
  • Test count: 192 (was 148 in v0.6.0). The +44 is 26 new unit tests (hooks install round-trips, diff parser fixtures, protected-branch glob matching, env override, audit JSONL parsing, RuleStats aggregator) and 18 new end-to-end integration tests (real tempdir git repos exercising install/uninstall idempotency, DROP DATABASE in a migration blocked, rm -rf / in a shell script blocked, force-push to main blocked, fast-forward allowed, audit-derived suggestion correctness, YAML splice shape).
Aperion Shield v0.7 git hooks demo: real repo, real GitHub remote, 8 scenarios — destructive SQL migrations refused, force-pushes to main refused, bypasses honoured, all in ~28 seconds.
Real repo. Real GitHub remote. 8 scenarios in ~28 seconds.
v0.6.0 (2026-05-18) — earlier in this minor line
  • aperion-shield --diff: native Rust pre-merge behaviour-diff explainer for shieldset changes. Runs the engine over the same corpus under two shieldsets and emits a per-rule attribution of which lines flipped — text, markdown (PR-comment friendly), or JSON. CI gates: --fail-if-flipped, --fail-if-loosened, --fail-if-allows-loosened N. See shieldset-as-code Layer 4.
  • Three Dependabot advisories closed: rustls-webpki 0.101.7 → 0.103.13 (transitive via reqwest 0.12 / rustls 0.23 / hyper 1.x). Closes RUSTSEC-2026-0098, -0099, -0104. cargo audit now clean against an empty ignore list.
  • OIDC callback server refactored to the hyper 1.x API. Identity-gated rules (ID.me, mock, Smartflow-mediated) work unchanged.

What it is

Aperion Shield is a local MCP middleman. You point your IDE at aperion-shield instead of pointing it directly at the upstream MCP server. Shield spawns the upstream as a child process, transparently proxies every JSON-RPC frame between IDE and tool, and intercepts tools/call requests so it can apply guardrail rules before they reach the tool.

One sentence: if your agent tries to run DROP TABLE customers;, rm -rf $HOME, git push --force main, an unscoped UPDATE users SET …, or a similarly destructive operation, Shield catches it on your machine and either blocks it or waits for you to type approve <ticket-id> in a terminal — before the upstream tool ever sees the call.

It is not a hosted service. There is nothing to sign up for. The binary is a single self-contained executable; the rule engine and a starter ruleset are baked in. No internet access is required at runtime.

Why it exists

An AI coding agent operating in your IDE is, in effect, an autonomous user with broad tool access — your database, your terminal, your git remotes, your filesystem, your cloud APIs. Existing controls miss the most dangerous category of failure:

LayerCatchesMisses
IDE permission promptsFirst-time access to a new tool / repoThe 200th call after you clicked "Always allow"
MCP server allowlistsTools the key isn't allowed to callAllowed-but-destructive calls
Your own code reviewBad code that lands in a PRTool calls that never produce a diff

Aperion Shield closes that gap with a single principle: any operation that can destroy data, history, or production state stops for a half-second pause. Either it's denied outright (Critical), or it sits in a local approval inbox until you type approve or deny (High), or it passes through with a warning logged (Medium).

How it works

Shield is a faithful MCP stdio bridge. It transparently relays every JSON-RPC frame except tools/call, which it intercepts:

┌──────────────┐   stdin/stdout   ┌────────────────────┐   stdin/stdout   ┌──────────────────────┐
│  Your IDE    │ ───────────────► │  aperion-shield    │ ───────────────► │  upstream MCP server │
│  (Cursor /   │ ◄─────────────── │  (rule engine +    │ ◄─────────────── │  (npx @mcp/...,      │
│  Claude Code)│                  │   approval inbox)  │                  │   uvx ..., custom)   │
└──────────────┘                  └────────────────────┘                  └──────────────────────┘

On a tools/call the engine walks the JSON-RPC params against each pre-compiled rule. SQL-aware extractors automatically pull from query, sql, or statement keys, so rules don't have to know each tool's parameter schema. If a rule fires, the engine returns one of four decisions:

Quick install

Pick whichever installation route fits your environment. All three produce the same aperion-shield binary.

Homebrew (macOS / Linux)

brew install AperionAI/tap/aperion-shield

Docker

docker run --rm -i \
  ghcr.io/aperionai/shield:latest \
  --help

Binary

# Linux / macOS / Windows
# x86_64 + aarch64
github.com/AperionAI/shield
   /releases

Cursor setup

Open ~/.cursor/mcp.json (or settings → MCP). Find the MCP server you want to protect (e.g. postgres) and rewrite it so the command becomes aperion-shield and the original command moves to args after a -- separator:

{
  "mcpServers": {
    "postgres-shielded": {
      "command": "aperion-shield",
      "args": [
        // Optional: start in shadow mode for the first day
        "--shadow",
        "--",
        "npx", "-y", "@modelcontextprotocol/server-postgres",
        "postgresql://localhost/mydb"
      ]
    }
  }
}

Restart Cursor. From the agent's perspective nothing has changed — the same tools appear, with the same names and schemas. Shield only steps in when a rule fires.

Claude Code setup

Open ~/.claude/config.json (or run claude mcp add) and wrap the upstream the same way:

{
  "mcpServers": {
    "github-shielded": {
      "command": "aperion-shield",
      "args": [
        "--rules", "/path/to/shield.yaml",
        "--",
        "npx", "-y", "@modelcontextprotocol/server-github"
      ],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." }
    }
  }
}

Cline · Continue · Windsurf · Zed (new in v0.7)

Same wrapper pattern, different config paths and (in one case) a different schema name:

IDEConfig fileSchema notes
Cline .vscode/cline_mcp_settings.json (per-workspace) or ~/.cline/cline_mcp_settings.json Same mcpServers shape as Cursor.
Continue ~/.continue/config.json Note: top-level mcpServers is an array, not a map. Each entry takes name, command, args.
Windsurf ~/.codeium/windsurf/mcp_config.json Identical to Cursor's mcpServers map schema.
Zed ~/.config/zed/settings.json Uses context_servers (not mcpServers) with a nested command: {path, args} shape. Set "command": {"path": "aperion-shield", "args": ["--", "npx", "-y", "@modelcontextprotocol/server-postgres"]}.

The README quickstart has fully-fleshed config snippets for each of these.

Docker

The published image at ghcr.io/aperionai/shield:latest is a multi-arch distroless image (~12 MB). Useful when running Shield inside a container alongside a containerised MCP server, or as a sidecar in CI:

docker run --rm -i \
  -v "$PWD/shield.yaml:/etc/aperion-shield/shield.yaml:ro" \
  -v "$PWD/.aperion-shield:/work/.aperion-shield" \
  -w /work \
  ghcr.io/aperionai/shield:latest \
  --rules /etc/aperion-shield/shield.yaml \
  -- \
  npx -y @modelcontextprotocol/server-postgres "$PG_URL"

Binary or build from source

# Download from GitHub Releases (Linux x86_64 / aarch64,
# macOS x86_64 / aarch64, Windows x86_64)
curl -L -o aperion-shield.tar.gz \
  https://github.com/AperionAI/shield/releases/download/shield-v0.7.0/aperion-shield-shield-v0.7.0-aarch64-apple-darwin.tar.gz
tar -xzf aperion-shield.tar.gz
./aperion-shield --version

# Or build from source — requires rustc 1.85+
git clone https://github.com/AperionAI/shield
cd shield
cargo build --release --locked
./target/release/aperion-shield --version

Operating modes

Shield runs in one of three modes, controlled by a CLI flag. The recommended path for any new ruleset is to start in --shadow for a day to surface false positives, then flip to enforce.

--shadow

Every decision is downgraded to allow + log. Use for the first day so you can read the warnings and tune the ruleset without breaking your workflow. Default for the bundled ruleset until you opt-in to enforce.

enforce (default)

Critical rules block; High rules send to the local approval inbox; Medium rules pass with a warning; Low rules pass silently. This is the steady-state mode once you trust the ruleset.

--auto-deny-high

Like enforce, but anything tagged High auto-denies instead of queueing for human approval. Use this on CI runners or unattended agent loops where there's no human to type approve.

Severity tiers and outcomes

SeverityOutcome (enforce)Returned to IDELatency on hit
CriticalHard blockJSON-RPC error shield_blockedNone — instant deny
HighLocal approval queueJSON-RPC error shield_approval_required + ticket_idUntil you type approve / deny (default timeout 60s)
MediumAllow with warnPass through; stderr warningNone
LowAudit onlyPass through silentlyNone

Local approval inbox

When a High-severity rule fires, Shield prints a structured warning to stderr (which Cursor and Claude Code surface in their tool-call panel) and waits up to 60 seconds for a decision in a local file:

# Cursor / Claude Code tool-call panel shows:
[shield] APPROVAL_REQUIRED rule='sql.unscoped_update' ticket='shld_4f17a3'
[shield] To approve, write 'approve shld_4f17a3' to ./.aperion-shield/inbox  (waiting 60s)

# In a terminal in your project directory:
echo "approve shld_4f17a3" >> .aperion-shield/inbox

# Or deny:
echo "deny shld_4f17a3" >> .aperion-shield/inbox
The inbox lives at ./.aperion-shield/inbox in whichever directory the IDE launched aperion-shield from (usually your project root). The file is append-only, line-oriented, and never read after the ticket either decides or times out — so it never grows unbounded.

On timeout, the call is denied by default. Set --approval-timeout 600 to wait longer, or use --auto-approve-low if you want Low/Medium to skip the inbox entirely (default already does this).

Built-in default ruleset — 45 rules, 12 categories

Shield ships with a 45-rule starter shieldset baked into the binary so the engine never starts ruleless. The corpus spans the destructive patterns AI coding agents most often produce: ad-hoc database surgery, git history rewrites, filesystem and credential touches, supply-chain footguns, privilege escalation, cloud-resource deletes, Kubernetes / Docker cluster-wipes, plus a sliding-window anomaly detector and five LLM-response “plan” matchers.

Every rule below is one that, when triggered, is almost certainly a mistake the agent didn't mean to make. Shield tunes toward zero false negatives on Critical and accepts the occasional Medium warning. To extend or tighten the corpus, write a shield.yaml with --rules (see Custom rules).
CategoryRule IDSeverityWhat it catches
SQL 9 sql.drop_databaseCriticalDROP DATABASE on any SQL tool
sql.drop_table_or_schemaHighDROP TABLE / DROP SCHEMA / TRUNCATE TABLE
sql.alter_table_drop_columnHighALTER TABLE … DROP COLUMN — irreversible & breaks downstream readers
sql.unscoped_deleteHighDELETE FROM <t> with no WHERE clause
sql.unscoped_updateHighUPDATE <t> SET … with no WHERE clause — or a tautological WHERE that selects exactly the rows the SET would change (e.g. WHERE col = FALSE paired with SET col = TRUE). v0.5.0
sql.grant_or_revoke_allMediumGRANT ALL / REVOKE ALL on schemas or roles
sql.revoke_from_publicHighREVOKE … FROM PUBLIC can lock out every role at once
sql.copy_from_programCriticalCOPY … FROM PROGRAM — Postgres RCE chain
sql.load_data_infileHighLOAD DATA INFILE — server-side file read / exfil
Git 5 git.force_push_protectedCriticalgit push --force to main/master/prod/release/*
git.history_rewriteHighgit filter-repo, filter-branch, reset --hard HEAD~
git.push_mirror_or_all_forceHighgit push --mirror / --all --force — every ref at once
git.branch_force_deleteMediumgit branch -D
git.checkout_dot_discardsMediumgit checkout . — discards every uncommitted change
Filesystem 6 fs.recursive_delete_rootCriticalrm -rf /, rm -rf $HOME, rm -rf ~, rm -rf $PWD
fs.sensitive_path_write_or_deleteHighWrite or delete touching /etc, /var, /usr, SSH dirs, credentials
fs.dd_to_block_deviceCriticaldd if=… of=/dev/sd*
fs.find_delete_sweepHighRecursive find … -delete — silent subtree sweep
fs.world_writable_chmodHighchmod 777 on sensitive paths
fs.chown_root_recursiveHighRecursive chown root — can lock the user out of their own files
Secrets 3 secret.env_to_networkCriticalRead a secret source & pipe to network sink in one line — exfil pattern
secret.read_ssh_or_aws_keyHighDirect read of ~/.ssh/id_*, ~/.aws/credentials, etc.
secret.cloud_kv_dumpHighBulk read of AWS Secrets Manager / SSM / GCP / Vault KV
Supply chain 2 supply.curl_pipe_shCriticalcurl | sh-style fetch-and-execute with no inspection
supply.untrusted_pkg_registryHighInstall from non-default registry — supply-chain / dependency-confusion risk
Reverse shell 1 shell.reverse_shellCriticalReverse-shell payload pattern — never auto-allowed
Privilege 2 privilege.sudo_destructiveHighSudo-prefixed destructive command — requires human approval
privilege.setuid_grantHighGranting setuid / extra capabilities — escalation risk
Cloud 5 cloud.aws_s3_recursive_deleteHighBulk S3 delete — irreversible if versioning is off
cloud.aws_rds_skip_snapshotCriticalRDS delete with no final snapshot — data is gone forever
cloud.terraform_destroy_auto_approveHighterraform destroy -auto-approve — skips human confirmation
cloud.gcloud_sql_deleteHighDelete a Cloud SQL instance — irreversible
cloud.az_group_deleteHighDelete an entire Azure resource group, skipping the confirmation
Kubernetes 4 k8s.delete_namespaceHighNamespace delete propagates to every resource inside
k8s.delete_allHighkubectl delete --all — rarely what you actually want
k8s.drain_nodeMediumDrain a node — every workload on it gets evicted
k8s.helm_uninstallMediumHelm uninstall — removes every workload from the release
Docker 2 docker.system_prune_aggressiveHighdocker system prune -af --volumes — caches AND volumes gone
docker.rm_force_volumesMediumForce-remove a container with its volumes — data inside is gone
Anomaly 1 anomaly.destructive_burstHigh5+ destructive tool calls per (actor, server, tool) in 5 min
LLM response 5 llm.suggests_drop_databaseHighAssistant plan contains destructive SQL (DROP DATABASE / TRUNCATE TABLE)
llm.suggests_force_pushMediumAssistant plan suggests force-push to a protected branch
llm.suggests_rm_rfMediumAssistant plan suggests rm -rf / or similar
llm.suggests_curl_pipe_shMediumAssistant plan suggests curl | sh-style install
llm.suggests_secret_exfilHighAssistant plan reads a secret and pipes it to a network endpoint
Counts at a glance: 9 SQL · 5 git · 6 filesystem · 3 secrets · 2 supply-chain · 1 reverse-shell · 2 privilege · 5 cloud · 4 Kubernetes · 2 Docker · 1 anomaly · 5 LLM-response = 45 rules.

Custom rules

Author your own rules in YAML and pass them with --rules:

aperion-shield --rules ./shield.yaml -- npx @modelcontextprotocol/server-postgres "$PG_URL"

Shield searches in this order when --rules isn't given:

  1. The path in SHIELD_RULESET_PATH
  2. ./.aperion-shield/shield.yaml (project-local)
  3. ~/.aperion-shield/shield.yaml (per-user)
  4. The built-in default ruleset

Rule schema — shield.yaml

shieldset:
  version: 1

  rules:
    # SQL — Critical (no WHERE clause is fine if you say DROP DATABASE)
    - id: sql.drop_database
      severity: Critical
      where: tool_call
      match:
        tool: ["execute_sql", "postgres.query", "mysql.query"]
        sql_matches: ['(?i)\bDROP\s+DATABASE\b']
      reason: "DROP DATABASE is never auto-allowed."

    # SQL — High (application-layer predicate; regex can't express "no WHERE")
    - id: sql.unscoped_update
      severity: High
      where: tool_call
      match:
        tool: ["execute_sql", "postgres.query", "mysql.query"]
        sql_predicates: ["unscoped_update"]
      reason: "Unbounded UPDATE affects every row. Also catches tautological WHERE clauses (e.g. `WHERE col = FALSE` paired with `SET col = TRUE`)."

    # Git — Critical
    - id: git.force_push_protected
      severity: Critical
      where: tool_call
      match:
        tool: ["run_terminal", "bash", "shell"]
        any_param_matches:
          - '\bgit\s+push\s+.*(--force|-f|\+)\s+\S+\s+(main|master|prod|release/[^\s]+)\b'
      reason: "Force-push to a protected branch is forbidden."

Match keys

Regex flavour: Shield uses Rust's regex crate, which does not support look-behind ((?<=…)) or back-references. Anything you can write in RE2 works. For patterns the regex engine can't express (like “UPDATE without WHERE”), use sql_predicates instead.

SQL predicates — when regex isn't enough

Some destructive SQL patterns require parsing the statement, not just regex-matching it. Shield ships application-layer predicates for the two most common cases:

PredicateWhat it matches
unscoped_updateUPDATE <table> SET … with no WHERE clause — or a tautological WHERE that selects exactly the rows the SET would change. Six tautology patterns are detected (boolean opposites, IS NULL paired with SET <value>, inequality paired with equality, etc.). Genuine scope-narrowing — WHERE created_at > NOW() - INTERVAL '7 days', WHERE tenant_id = $1 — passes through. v0.5.0
unscoped_deleteDELETE FROM <table> with no WHERE clause — would delete every row.
These are evaluated on the parsed SQL, so trailing comments, multiple statements, and quoting tricks don't fool them. The classic agent failure mode — UPDATE users SET email_verified = true; -- forgot the WHERE — is reliably caught. New in v0.5.0: the rule also catches the agent's favourite work-around, UPDATE users SET email_verified = TRUE WHERE email_verified = FALSE, where the WHERE selects exactly the rows the SET would change — functionally equivalent to no WHERE. AND-combined predicates are handled conservatively: if any branch narrows scope by something other than the SET target, the call passes.

Adaptive scoring — context-aware severity (v0.2+)

The 45-rule base corpus tells Shield what is potentially destructive. The adaptive layer tells Shield how serious this specific match is right now. Every rule hit runs through four lightweight signals and a composite scorer before producing a final decision. Each signal can be disabled with a flag if you'd rather have raw rule output.

SignalWhat it doesHow it adjusts severityDisable flag
Workspace probeSniffs the working dir for “prod-looking” markers: .env.production, terraform/prod, docker-compose.prod.yaml, a non-localhost DATABASE_URL, etc.+1 tier on a hit (Medium → High, High → Critical)--no-workspace-probe
Decision memoryHashes (rule_id, tool, redacted_param_fingerprint). Remembers the last N approvals and denials per fingerprint on disk.−1 tier after 3 approvals of the same fingerprint; +1 tier after a recent deny--no-memory
Burst detectorSliding window over destructive matches across all rules; runs in-process (no Redis needed).+1 tier while a wave is in progress, plus the anomaly.destructive_burst rule fires once the threshold is crossed--no-burst
Composite scorerSums the raw rule severity (Critical=4, High=3, Medium=2, Low=1) plus signal adjustments and maps the total back to a final tier.Reported in the JSON output as composite_severity, composite_points, and adjustments[]— (always on; disabling all signals leaves it a no-op)

Workspace probe

Runs once at startup, cached for the lifetime of the process. Sees only file names (never contents) inside the working directory tree. The list of prod markers is open source and easy to extend; pull requests welcome.

Decision memory

Stored at ./.aperion-shield/memory.json — append-only, line-oriented, capped at 1 MB. After 3 approvals of the same destructive-but-routine call (e.g. a daily find ./logs -delete by the same agent on the same machine), Shield demotes that fingerprint by one tier and reports memory_demote as an adjustment. A recent deny of the same fingerprint within 24h promotes by one tier.

Burst detector

In-process sliding-window counter over destructive matches (any SQL/git/fs/cloud/k8s rule). When the count crosses the threshold inside the configured window, every subsequent destructive match within that window gets a burst_bump adjustment. Storage is in-memory only — single-process by design.

# Example JSON output when adaptive layer kicks in
{
  "decision": "block",
  "primary_rule_id": "fs.find_delete_sweep",
  "raw_severity": "High",
  "composite_severity": "Critical",
  "composite_points": 5,
  "adjustments": ["workspace_is_prod", "burst_bump"],
  "reason": "Recursive find ... -delete can sweep an entire subtree silently.",
  "safer_alternative": "Run `find ... -print` first, eyeball the list, then re-run with `-delete`."
}

Identity-gated rules (v0.4+)

For the most dangerous category of operations — production database writes, signed commits to release branches, customer-data exports — Shield can require a verified human identity before allowing the call through. Approving from the local inbox is not enough; an actual person has to complete an OAuth flow and prove they are who they say they are.

Add an identity: block to any rule. The engine will return a new decision variant — identity_verification_required — which holds the tool call until the user finishes the verification flow in their browser. A signed proof is cached locally for the duration the rule says.

shieldset:
  version: 1

  rules:
    - id: scm.commit_to_main
      severity: High
      where: tool_call
      match:
        tool: ["run_terminal", "bash"]
        any_param_matches:
          - '\bgit\s+commit\b.*\b(main|master|release/)'
      identity:
        provider: idme              # or: mock, okta, custom
        required: true
        scope: scm.commit
        max_age_seconds: 900      # proof good for 15 min
        loa: 2                    # NIST Identity Assurance Level
        allowed_subjects:           # optional allow-list (sub, email, idme|sub)
          - "idme|abc123-def456"
          - "[email protected]"
      reason: "Commits to main require a verified human."
Proofs are signed with an ed25519 keypair generated on first use (stored at ~/.aperion-shield/identity-key, mode 0600). The cache key is (subject, scope) — the same verified user re-using the same scope inside max_age_seconds hits the cache and never sees the browser flow twice.

Built-in providers

ProviderKindStatusWhat it's for
mockAlways-verifyReadyTests, demos, smoke runs. Signs a real proof but never prompts; ships an explicit warning every time it's used.
idmeOAuth 2.0 + PKCE, ID.me as IdPSandbox pendingNIST IAL2 biometric verification. Production. Set IDME_CLIENT_ID / IDME_CLIENT_SECRET to activate; reports ready=false until both are present.
oktaOIDCRoadmapEnterprise SSO; uses your existing Okta tenant as the verifier.
customTrait-implementedReadyImplement the IdentityProvider trait against any IdP that returns a signed claim. ~80 LOC of Rust.

identity.yaml — provider configuration

Provider configuration is discovered in this order: --identity-config <PATH>$APERION_SHIELD_IDENTITY_CONFIG~/.aperion-shield/identity.yaml → built-in mock-only defaults.

providers:
  - id: idme
    kind: id_me
    client_id_env: IDME_CLIENT_ID
    client_secret_env: IDME_CLIENT_SECRET
    sandbox: true              # swap to false in production

  - id: dev
    kind: mock
    always_verify: true        # helpful for local dev only
Identity is opt-in per rule. No rule in the default shieldset carries an identity: block today — adding identity gates is an explicit policy choice and the binary will warn loudly on startup if you ask for an identity provider that isn't configured. Disable the whole subsystem with --no-identity if you want to ship rules but turn the gate off.
# Cache inspection / management
aperion-shield --identity-list      # show cached proofs (sub, scope, expires_at)
aperion-shield --identity-flush     # drop every cached proof

Org Mode — central policy via Smartflow (v0.5+)

For teams that want one policy across every developer laptop and every CI runner, aperion-shield can be enrolled against a Smartflow control plane. Once enrolled, the binary pulls its shieldset from Smartflow, ships audit events back, and delegates identity verification through the central dashboard. Standalone mode (no enrollment record) still works exactly the same — Org Mode is purely additive.

The trust boundary is a per-device verification key (vkey) issued at enrollment and stored at ~/.aperion-shield/orgmode.json with mode 0600. Every request to the Smartflow control plane is HMAC-authed with that vkey.

# 1) Issue a one-time enrollment token from the Smartflow dashboard:
#    https://<your-smartflow>/dashboard/shield_fleet.html → "Enroll a device"

# 2) Enroll on the laptop / runner:
aperion-shield \
  --enroll \
  --smartflow-url https://smartflow.example.com \
  --token enroll_o7s9...3jk \
  --device-name "alice-laptop" \
  --enroll-email [email protected]

# 3) Verify:
aperion-shield --status
# → enrolled  device=alice-laptop  group=engineering  policy=v17  killswitch=off

# Run as normal -- policy + identity + audit now go through Smartflow:
aperion-shield -- npx -y @modelcontextprotocol/server-postgres "$PG_URL"

Central policy with hot-reload

Once enrolled, the binary polls Smartflow every 30 seconds for the active shieldset version of its policy group. On a version bump it fetches the new YAML, compiles a new engine, and atomically swaps it in via a tokio::sync::watch channel — no restart needed, no in-flight tool call is interrupted. Failures fall back to the previously cached policy; if everything is unreachable the binary falls back to the built-in 45-rule defaults.

Audit shipping & fleet killswitch

Every actionable decision (Block / Approval / Warn / Identity) is buffered and POSTed in batches every 5 seconds to /api/enterprise/shield/events. On graceful shutdown the in-flight batch is drained before exit. The dashboard's Shield Settings page renders a live, filterable timeline of these events across the entire fleet.

An admin can flip the fleet-wide killswitch from the dashboard. Enrolled devices pick it up on the next 30-second poll and downgrade every decision to AllowWithWarn until the switch is cleared — useful for incident response when a rule is misbehaving.

Disenrolling turns the binary back into a vanilla standalone immediately: aperion-shield --disenroll (add --revoke to also revoke the vkey server-side). The local orgmode.json is shredded and the next start uses the built-in defaults.

Shieldset-as-code: the --diff behaviour explainer (v0.6+)

Every rule change is a behaviour change against thousands of real agent commands. Tightening one regex can add 50 approval prompts to your team's day. Loosening one can silently let a destructive call through. Neither should land without someone reading the diff and someone else verifying the impact.

aperion-shield --diff closes that gap. It runs the engine over the same JSON-Lines corpus under two different shieldsets — once with the current rules, once with the proposed rules — and emits a per-rule attribution of which lines flipped because of which rule change.

aperion-shield --diff \
  --rules-before main-shieldset.yaml \
  --rules-after  pr-shieldset.yaml \
  --corpus       tests/corpus/team-cursor-history.jsonl \
  --format       markdown

Three output formats:

Three CI gate flags:

FlagExits 1 whenUse it for
--fail-if-flippedAny decision changedStrict gate on critical-tier shieldsets where any behaviour change needs explicit sign-off
--fail-if-loosenedAny line moved toward a more permissive decisionThe gate most teams want — tightening is fine, loosening requires a human
--fail-if-allows-loosened NMore than N lines flipped to allowPermits warn→allow on a case-by-case basis up to a threshold
How it works under the hood. Both engine runs happen in-process — no subprocess, no PATH lookup, no JSON re-encode/re-decode trip per line. The diff mode reuses the same Engine::evaluate path the proxy uses, so the decisions in the report are exactly the decisions a live wrapped agent would see against either shieldset. Memory and burst detector are disabled in diff runs (they're stateful and would make the two engines non-comparable); the workspace probe stays on.

Full PR-review pattern, including the four-layer test stack (load · golden corpus · workflow corpus · behaviour diff): docs/shieldset-as-code.md.

Git hooks: --install-hooks (new in v0.7)

The single biggest objection to MCP-only enforcement, repeated on HN and in every Cline/Continue thread, is the same: "the agent just opens a shell and reaches around your guardrail." v0.7 closes that gap by running the same engine on the way to git — regardless of whether the destructive change came from an MCP tools/call, a generated migration, or a direct shell command an agent ran in your terminal.

Install in any repo (~1 second).
$ aperion-shield --install-hooks
[aperion-shield] writing pre-commit  -> .git/hooks/pre-commit
[aperion-shield] writing pre-push    -> .git/hooks/pre-push
[aperion-shield] both hooks installed (managed by APERION-SHIELD-HOOK v1)
Re-running is safe: the hooks are idempotent and replace only Shield-managed installs (marked with an APERION-SHIELD-HOOK banner). If a husky / pre-commit / lefthook hook is already present, Shield refuses to clobber it; pass --chain-existing to back the existing hook up to .aperion-backup and chain Shield in front of it.

--check-staged (pre-commit)

The installed pre-commit hook calls aperion-shield --check-staged, which:

  1. Runs git diff --cached --unified=0 and feeds every added/modified line through the engine, classified by file kind (.sql → SQL evaluator, .sh / Dockerfile / Makefile → shell evaluator, source files → code-path evaluator).
  2. Skips comment-only lines, pure whitespace, and any file exceeding the configurable size cap (default 2 MB, override with SHIELD_HOOK_MAX_BYTES).
  3. Exits with a meaningful exit code: 0 = clean, 1 = at least one Block verdict (commit refused), 2 = at least one Approval verdict (commit refused — approvals are an MCP-runtime concept, not a commit-time one), 3 = operational error (engine load failure, etc.).

Example block from a real .sql migration:

$ git add migrations/2026_05_20_drop_user_table.sql
$ git commit -m "WIP"
[aperion-shield/check-staged] blocking 1 finding:

  migrations/2026_05_20_drop_user_table.sql:3
    rule: sql.drop_database  (severity=Critical)
    line: DROP DATABASE app_prod;
    why : DROP DATABASE is non-recoverable; use a separate ops runbook,
          not a checked-in migration.

[aperion-shield] commit refused. bypass options:
  - git commit --no-verify           (one-time, your decision)
  - SHIELD_HOOKS_DISABLE=1 git commit (one-time, env override)
  - aperion-shield --uninstall-hooks  (remove entirely)

--check-pushed-refs (pre-push)

The installed pre-push hook calls aperion-shield --check-pushed-refs, which reads git's pre-push stdin protocol (<local-ref> <local-sha> <remote-ref> <remote-sha> per line) and refuses the push if:

Same --no-verify and SHIELD_HOOKS_DISABLE=1 bypasses apply.

Coexistence with husky / pre-commit / lefthook

--install-hooks looks at the existing .git/hooks/<name> file before writing. Three outcomes:

Uninstall is symmetrical: aperion-shield --uninstall-hooks removes only Shield-managed files and restores any .aperion-backup sidecar it created on install.

Tune your shieldset from your own audit log: --suggest-rules (new in v0.7)

Static rule sets drift. After two weeks of real use, some rules never fire (your team genuinely never runs that operation), some get approved-and-demoted 9 times out of 10 (the rule is wrong about the local risk tier), and some Warn rules are pure noise. --suggest-rules reads your own shield_eval JSON-Lines audit log and tells you, with rationale, what to change.

Capture, then analyse

# 1. Redirect Shield's audit-log stream to a file
$ aperion-shield -- <upstream-cmd> 2> ~/.aperion-shield/audit.jsonl & ...
# (use Shield normally for at least a few hundred operations)

# 2. Ask for suggestions
$ aperion-shield --suggest-rules \
    --audit-log ~/.aperion-shield/audit.jsonl \
    --suggest-window-days 14 \
    --suggest-min-occurrences 10 \
    --suggest-format yaml-patch

Three suggestion classes

ClassWhen it firesWhat it suggests
RULE_NEVER_FIRES Rule exists in your shieldset but has zero matches in the analysis window Consider removing — or document why it exists as a defence-in-depth tripwire
CONSISTENTLY_DEMOTED ≥ N matches, > 70% of decisions were Allow/AllowWithWarn after the adaptive layer downgraded the rule Drop the rule's severity by one tier, or refine the predicate so it stops catching legitimate work
NOISY_WARN Rule with severity: Warn matches very frequently with no human intervention recorded Either drop to severity: Info or remove — repeated warnings train your team to ignore the audit log

Output formats

This stays local. The analyser never reads from a network source and never writes anywhere except the file you ask it to. --suggest-rules is a static analysis pass over your own audit log — Shield does not phone home, and v0.7 does not change that.

Aperion Shield vs Smartflow Shield (enterprise)

Aperion Shield is the free, local edition. Smartflow's enterprise Shield is the same rule engine wired into Smartflow's gateway — same YAML schema, same severity model — but with the things you need when teams of agents start running in production.

CapabilityAperion Shield (free)Smartflow Shield (enterprise)
Local MCP wrapping
45-rule starter shieldset15-rule starter (proxy-resident); extend with your own YAML
LLM-response inspection (catches plans before they run)5 rules
Sliding-window burst detectorin-processRedis-backed
Adaptive scoring (workspace probe, decision memory, composite)
Identity-gated rules (ID.me / Okta / mock / custom)local OAuthorg-mediated
One-shot --check mode (CI / batch validation)
Org-mode enrollment with Smartflow control planeclient sideserver side
Team-wide centrally managed shieldset (hot-reload)via Org Mode
Fleet dashboard (devices, policies, audit timeline, killswitch)
Web-UI approval queue (no terminal needed)
Tamper-evident HMAC-chained audit log
SSO / OIDC approver identity
Spend & activity dashboards
Same YAML rule schema
LicenseApache 2.0Commercial
CostFree, foreverPer-seat or per-deployment
Org Mode bridges the two. An aperion-shield binary enrolled against Smartflow gets the centrally managed shieldset, fleet dashboard, and HMAC-chained audit chain on the server side while keeping the local rule engine, identity gate, and adaptive layer client-side. See Org Mode above.

The expected progression is: a developer installs Aperion Shield on their laptop, validates that the ruleset catches the right things on real workflows, then their company adopts Smartflow when they need team-wide observability, the approval dashboard, and centrally managed policy.

Privacy & telemetry

Aperion Shield is offline by default. It does not phone home, does not check for updates, does not send rule hits anywhere. Your code, your SQL, your shell commands, your approval decisions never leave your machine.

A future opt-in flag (--telemetry) is reserved for an anonymised public block ticker — a once-per-minute aggregate count of (rule_id, severity) tuples sent to a community endpoint, intended to power a public dashboard like a CVE tracker. It is currently stubbed and gated on legal/DPO review; setting the flag today prints a notice and exits without sending anything.

CLI flags

aperion-shield [OPTIONS] -- <upstream-mcp-command> [args...]

Rules & engine
  --rules <PATH>            Path to a shield.yaml ruleset. Falls back to
                            $SHIELD_RULESET_PATH, ./.aperion-shield/shield.yaml,
                            ~/.aperion-shield/shield.yaml, then the built-in
                            45-rule default.

  --shadow                  Downgrade every decision to allow+log. Useful for
                            the first day of a new ruleset to surface false
                            positives without breaking workflows.

  --auto-deny-high          Auto-deny any High-severity hit instead of queueing
                            for human approval. Use in CI or unattended agents.

Adaptive layer
  --no-workspace-probe      Disable the workspace prod-marker probe.
  --no-memory               Disable the decision-memory layer.
  --no-burst                Disable the in-process burst detector.

Identity gating
  --identity-config <PATH>  Override the default identity.yaml discovery.
  --no-identity             Disable identity gating entirely; rules with
                            an identity: block fall back to Block/Approval.
  --identity-list           Print cached identity proofs and exit.
  --identity-flush          Drop every cached identity proof and exit.

Org Mode (v0.5+)
  --enroll                  Enroll against a Smartflow control plane.
                            Requires --smartflow-url and --token.
  --smartflow-url <URL>     Smartflow base URL (with --enroll).
  --token <TOKEN>           One-time enrollment token (with --enroll).
  --device-name <NAME>      Friendly device name (with --enroll).
  --enroll-email <EMAIL>    Owner email (with --enroll).
  --status                  Print enrollment status and exit.
  --disenroll               Remove the local enrollment record.
  --revoke                  With --disenroll, also revoke the vkey
                            server-side via DELETE /api/enterprise/devices/{id}.

One-shot evaluation
  --check                   Read tool-call descriptors from stdin (one JSON
                            object per line) and print the engine's decision
                            for each as JSON. No MCP / upstream needed.
                            Exit 0 if all expectations met, 1 otherwise.
  --workspace <PATH>        Override the workspace root for --check / --diff.

Behaviour diff (v0.6+)
  --diff                    Run the engine over a corpus twice (under two
                            shieldsets) and emit a per-rule attribution of
                            which lines flipped. The pre-merge review tool.
  --rules-before <PATH>     Current (main-branch) shieldset YAML.
  --rules-after  <PATH>     Proposed (PR-branch) shieldset YAML.
  --corpus <PATH>           JSON-Lines corpus path (default: stdin).
  --format <FMT>            text | markdown | json (default: text).
  --max-samples <N>         Flipped-line samples per rule (default: 3).
  --fail-if-flipped         Exit 1 if any line's decision changed.
  --fail-if-loosened        Exit 1 if any line moved to a more permissive
                            decision. The CI gate most teams want.
  --fail-if-allows-loosened <N>
                            Exit 1 if more than N lines flipped to `allow`.

Git hooks (v0.7+)
  --install-hooks           Write managed pre-commit + pre-push hooks into
                            .git/hooks/. Idempotent. See --chain-existing
                            for husky / pre-commit / lefthook coexistence.
  --uninstall-hooks         Remove Shield-managed hooks and restore any
                            .aperion-backup sidecars created on install.
  --repo <PATH>             Operate on the given repo root (default: cwd).
  --chain-existing          On --install-hooks, if a foreign hook is
                            present, rename it to .aperion-backup and
                            chain Shield in front of it. Without this
                            flag, Shield refuses to clobber.
  --check-staged            Engine pass over `git diff --cached` output.
                            Used by the installed pre-commit hook.
                            Exit 0=clean, 1=Block, 2=Approval, 3=error.
  --check-pushed-refs       Engine pass over git's pre-push stdin format.
                            Used by the installed pre-push hook.
                            Refuses force-pushes / protected-branch
                            deletions. Override protected branches with
                            SHIELD_PROTECTED_BRANCHES=trunk,deploy/*.

Audit-driven rule tuning (v0.7+)
  --suggest-rules           Read a shield_eval audit log (JSON Lines) and
                            print rule-tuning suggestions: rules that
                            never fire, rules consistently demoted by
                            the adaptive layer, and noisy Warn rules.
  --audit-log <PATH>        Path to the captured audit log.
  --suggest-window-days <N> Only consider records within the last N days.
  --suggest-min-occurrences <N>
                            Threshold under which a rule is too sparse
                            to make a recommendation. Default 5.
  --suggest-format <FMT>    text | markdown | yaml-patch (default: text).
                            `yaml-patch` produces splice-ready snippets
                            for shieldset.yaml with rationale comments.

Privacy / telemetry
  --telemetry <MODE>        [Reserved] Opt in to the anonymised public block
                            ticker. Currently prints a notice and exits.

Misc
  --version                 Print version and exit.
  --help                    Print this help and exit.

Troubleshooting

The IDE shows “MCP server failed to start”

Run the same command in a terminal — Shield's startup error (typically a missing upstream command or a malformed shield.yaml) prints on stderr. Most failures are misquoted args between aperion-shield and the -- separator.

I want to bypass Shield for a single dangerous call I really mean

Use the local approval inbox: when Shield prompts, type approve <ticket-id> in a terminal in your project root. There is no global “disable” switch — that's intentional.

My MCP server uses a non-stdio transport (SSE / HTTP)

v0.7.0 supports stdio only. SSE / HTTP MCP transports are on the roadmap; track GitHub issues.

An identity-gated rule says provider not ready

The provider you referenced in the rule's identity: block is configured but missing credentials. For idme, set IDME_CLIENT_ID and IDME_CLIENT_SECRET in the environment. aperion-shield --identity-list will report the readiness state of every configured provider.

Org Mode: policy isn't hot-reloading

Policy is polled every 30 s. After publishing a new version in the dashboard, wait one poll cycle (look for hot-reloaded policy: group=… version=… rules=… on stderr). If you don't see the log line, check aperion-shield --status for connectivity; falling back to the previously cached policy is the design.