Skip to main content

Terminal Demo: Governance API — Allow, Deny & Secret Detection

Call the governance endpoint directly with curl. Test scope-based rules, secret detection against AWS keys, GitHub PATs, and private keys, and see every decision on the dashboard.

Overview

These demos call the governance endpoint directly with curl. No agent session required. Each call evaluates the same governance pipeline that runs during live Claude Code sessions and returns an allow or deny decision with the evaluation trace.

Prerequisites: Get your plugin token from Setup & Authentication — Step 3:

TOKEN="<paste-your-plugin-token-here>"
URL="http://localhost:8080"  # or your deployed instance URL

The Governance Endpoint

POST /api/public/hooks/govern?plugin_id=enterprise-demo
Authorization: Bearer <token>
Content-Type: application/json

The request body describes the tool call being evaluated:

{
  "hook_event_name": "PreToolUse",
  "tool_name": "<tool-name>",
  "agent_id": "<agent-name>",
  "session_id": "<session-id>",
  "tool_input": { }
}

The response contains the governance decision, reason, and the list of rules evaluated.


Part 1: Governance Allows — Admin Scope

An admin-scope agent calling an MCP tool it has access to:

curl -s -X POST "$URL/api/public/hooks/govern?plugin_id=enterprise-demo" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "mcp__systemprompt__list_agents",
    "agent_id": "developer_agent",
    "session_id": "demo-governance-happy"
  }' | python3 -m json.tool

Expected: decision: allow — all governance rules passed. The developer_agent has admin scope and the systemprompt MCP server is in its configuration.

Why three independent rules: The rule engine evaluates scope_check, secret_injection, and rate_limit independently. Even if scope allows access, secret injection still blocks leaked credentials. Rate limiting prevents runaway agents. Rules return typed Vec<RuleEvaluation> structs, not loose JSON.

Why async audit: tokio::spawn fires the database write without blocking the HTTP response. The governance decision returns in ~12ms while the audit record writes asynchronously. This keeps the hot path fast — the caller never waits for the audit INSERT.


Part 2: Governance Denies — Scope Check

The same tool, but requested by a user-scope agent:

curl -s -X POST "$URL/api/public/hooks/govern?plugin_id=enterprise-demo" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "mcp__systemprompt__list_agents",
    "agent_id": "associate_agent",
    "session_id": "demo-governance-denied"
  }' | python3 -m json.tool

Expected: decision: deny — the scope_check rule failed. The associate_agent has user scope, which does not grant access to admin-only MCP tools.

Why short-circuit evaluation: The first rule failure stops evaluation. No point checking secrets or rate limits if scope already denies. This keeps governance fast and avoids unnecessary work.

Why denials are audited: Denied calls are audited with the same fidelity as approvals. The evaluated_rules JSONB column stores exactly which rule failed and why. This is critical for compliance — you need to prove that denials happen correctly.

Dashboard

Open /admin/governance. Both decisions appear with timestamps, the tool name, agent ID, and the full evaluation trace.


Part 3: Secret Detection

The governance layer scans every tool_input field for plaintext secrets. If a secret pattern is detected, the call is blocked immediately — regardless of agent scope.

Test 1: AWS Access Key

curl -s -X POST "$URL/api/public/hooks/govern?plugin_id=enterprise-demo" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "Bash",
    "agent_id": "developer_agent",
    "session_id": "demo-secret-breach",
    "tool_input": {
      "command": "curl -H \"Authorization: <AWS_ACCESS_KEY>\" https://s3.amazonaws.com/bucket",
      "description": "Fetch S3 object"
    }
  }' | python3 -m json.tool

Expected: decision: deny — secret_injection detected (AWS access-key prefix pattern).

Test 2: GitHub Personal Access Token

curl -s -X POST "$URL/api/public/hooks/govern?plugin_id=enterprise-demo" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "Write",
    "agent_id": "developer_agent",
    "session_id": "demo-secret-breach",
    "tool_input": {
      "file_path": "/home/user/.env",
      "content": "GITHUB_TOKEN=<GITHUB_PAT>\nDATABASE_URL=postgres://localhost/db"
    }
  }' | python3 -m json.tool

Expected: decision: deny — secret_injection detected (GitHub PAT prefix pattern).

Test 3: RSA Private Key

curl -s -X POST "$URL/api/public/hooks/govern?plugin_id=enterprise-demo" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "Write",
    "agent_id": "developer_agent",
    "session_id": "demo-secret-breach",
    "tool_input": {
      "file_path": "/home/user/.ssh/id_rsa",
      "content": "<RSA_PRIVATE_KEY>"
    }
  }' | python3 -m json.tool

Expected: decision: deny — secret_injection detected (private key header).

Test 4: Clean Input — Passes

curl -s -X POST "$URL/api/public/hooks/govern?plugin_id=enterprise-demo" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "Read",
    "agent_id": "developer_agent",
    "session_id": "demo-secret-breach",
    "tool_input": {
      "file_path": "/home/user/project/src/main.rs"
    }
  }' | python3 -m json.tool

Expected: decision: allow — no secrets detected, all rules passed.

Why recursive scanning: The secret scanner traverses serde_json::Value recursively — ALL nested strings in tool_input are checked, not just top-level fields. An attacker can't hide a key in a nested JSON object.

Why audit without leaking: The audit record stores the secret TYPE ("AWS access key") but NOT the actual secret value. Typed Rust structs enforce this separation at compile time.


Summary

# Tool Agent Secret Type Decision
1 mcp__systemprompt__list_agents developer_agent allow
2 mcp__systemprompt__list_agents associate_agent deny (scope_check)
3 Bash developer_agent AWS Access Key deny (secret_injection)
4 Write developer_agent GitHub PAT deny (secret_injection)
5 Write developer_agent RSA Private Key deny (secret_injection)
6 Read developer_agent None (clean) allow

Dashboard

Open /admin/governance. All 6 decisions are visible with:

  • Timestamp
  • Tool name and agent ID
  • Decision (allow/deny)
  • Policy that triggered the denial
  • Full evaluation trace

Governance Evaluation Pipeline

Every tool call passes through these rules in order. Any failure short-circuits the evaluation:

Rule What It Checks Short-circuits?
secret_detection Scans tool_input for 37 secret patterns (AWS keys, GitHub tokens, Stripe keys, database connection strings, private keys, JWT tokens). Policy: secret_injection Yes — immediate deny
scope_check Agent scope vs tool requirements (admin tools require admin scope). Policy: scope_restriction Yes — immediate deny
tool_blocklist Destructive operation patterns (delete/drop/destroy) for non-admin scopes. Policy: tool_blocklist Yes — immediate deny
rate_limit Call frequency per session (300 calls/min). Policy: rate_limit Yes — immediate deny

Secret detection runs first. Even an admin-scope agent is blocked if tool input contains a plaintext secret. All four rules are evaluated independently; the first failure short-circuits evaluation.


Quick Demo: Copy-Paste Script

Copy this entire block into Claude Code (or any terminal) to run all six governance tests in sequence. Replace YOUR_TOKEN_HERE with the plugin token from Setup — Step 3.

TOKEN="YOUR_TOKEN_HERE"
URL="http://localhost:8080"  # or your deployed instance URL
API="$URL/api/public/hooks/govern?plugin_id=enterprise-demo"

# ── Test 1: Admin scope → ALLOW ─────────────────────────────────
# The developer_agent has admin scope, so it can call any MCP tool.
echo "── Test 1: Admin agent calls admin MCP tool ──"
curl -s -X POST "$API" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "mcp__systemprompt__list_agents",
    "agent_id": "developer_agent",
    "session_id": "demo-governance"
  }' | python3 -m json.tool
echo ""

# ── Test 2: User scope → DENY (scope_restriction) ──────────────
# The associate_agent has user scope — admin-only tools are blocked.
echo "── Test 2: User agent calls admin MCP tool ──"
curl -s -X POST "$API" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "mcp__systemprompt__list_agents",
    "agent_id": "associate_agent",
    "session_id": "demo-governance"
  }' | python3 -m json.tool
echo ""

# ── Test 3: AWS key in tool input → DENY (secret_injection) ────
# The governance layer scans every field in tool_input for secrets.
# An AWS access key (AWS access-key prefix) is detected and the call is blocked
# before the tool ever executes.
echo "── Test 3: Bash command contains AWS access key ──"
curl -s -X POST "$API" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "Bash",
    "agent_id": "developer_agent",
    "session_id": "demo-governance",
    "tool_input": {
      "command": "curl -H \"Authorization: <AWS_ACCESS_KEY>\" https://s3.amazonaws.com/bucket",
      "description": "Fetch S3 object"
    }
  }' | python3 -m json.tool
echo ""

# ── Test 4: GitHub PAT in file content → DENY (secret_injection)
# A Write tool call attempts to create a .env file containing a
# GitHub personal access token (GitHub PAT prefix). Blocked immediately.
echo "── Test 4: Write .env file containing GitHub PAT ──"
curl -s -X POST "$API" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "Write",
    "agent_id": "developer_agent",
    "session_id": "demo-governance",
    "tool_input": {
      "file_path": "/home/user/.env",
      "content": "GITHUB_TOKEN=<GITHUB_PAT>\nDATABASE_URL=postgres://localhost/db"
    }
  }' | python3 -m json.tool
echo ""

# ── Test 5: RSA private key → DENY (secret_injection) ──────────
# Even an admin-scope agent is blocked. Secret detection runs first
# and overrides all other rules.
echo "── Test 5: Write SSH private key file ──"
curl -s -X POST "$API" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "Write",
    "agent_id": "developer_agent",
    "session_id": "demo-governance",
    "tool_input": {
      "file_path": "/home/user/.ssh/id_rsa",
      "content": "<RSA_PRIVATE_KEY>"
    }
  }' | python3 -m json.tool
echo ""

# ── Test 6: Clean input → ALLOW ─────────────────────────────────
# A normal Read call with no secrets in the input. All four rules
# pass and the tool is allowed to execute.
echo "── Test 6: Read a source file (clean input) ──"
curl -s -X POST "$API" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_event_name": "PreToolUse",
    "tool_name": "Read",
    "agent_id": "developer_agent",
    "session_id": "demo-governance",
    "tool_input": {
      "file_path": "/home/user/project/src/main.rs"
    }
  }' | python3 -m json.tool

# ── Check the dashboard ─────────────────────────────────────────
echo ""
echo "All 6 decisions are now visible at:"
echo "  /admin/governance"
echo ""
echo "Expected results:"
echo "  Test 1: ALLOWED  (admin scope, clean input)"
echo "  Test 2: DENIED   (user scope → scope_restriction)"
echo "  Test 3: DENIED   (AWS key → secret_injection)"
echo "  Test 4: DENIED   (GitHub PAT → secret_injection)"
echo "  Test 5: DENIED   (RSA key → secret_injection)"
echo "  Test 6: ALLOWED  (admin scope, clean input)"

What to show the audience

After running the script, open /admin/governance. The metric ribbon updates immediately:

  • Total Decisions increases by 6
  • Denied increases by 4 (scope + 3 secrets)
  • Secret Breaches increases by 3

Scroll the table to show the decision trail. Each row shows the tool name, the agent that requested it, the decision badge (ALLOWED / DENIED / SECRET BREACH), the policy that triggered the denial, and the timestamp. Click any user ID to see their full activity history.

The key takeaway: the same governance pipeline that blocked these curl calls runs on every tool call in a live Claude Code session. Policies evaluate content in real time, secrets are caught before the tool executes, and every decision is auditable.


Audit

Verify governance decisions were recorded correctly:

# Governance: Check most recent allow/deny decisions
systemprompt infra db query \
  "SELECT decision, tool_name, policy, reason FROM governance_decisions ORDER BY created_at DESC LIMIT 5"

# Governance: Verify secret breach counts (should be 3 deny + 1 allow)
systemprompt infra db query \
  "SELECT decision, COUNT(*) as count FROM governance_decisions WHERE session_id = 'demo-secret-breach' GROUP BY decision ORDER BY decision"

# Full detail for secret breach tests
systemprompt infra db query \
  "SELECT decision, tool_name, policy, reason FROM governance_decisions WHERE session_id = 'demo-secret-breach' ORDER BY created_at"

Expected results:

Test Decision Policy Reason
Governance allow allow default_allow admin scope, all rules passed
Governance deny deny scope_restriction user scope cannot access admin tools
Secret breach (tests 1-3) deny secret_injection AWS key / GitHub PAT / PEM key detected
Secret breach (test 4) allow default_allow clean input, no secrets found

Next

Run MCP Access Tracking to combine governance calls with live MCP tool execution and database audit queries.