{
  "markdown": "# signet-eval\n\nDeterministic policy enforcement for AI agent tool calls. Every action an agent proposes passes through user-defined rules before execution. No LLM in the authorization path. Advisory nudges are separate from authorization. 25ms end-to-end.\n\n## Install\n\n```bash\n# crates.io\ncargo install signet-eval\n\n# from source\ngit clone https://github.com/jmcentire/signet-eval\ncd signet-eval\ncargo install --path .\n```\n\nThere is no npm or PyPI package for signet-eval. The public distribution path is\ncrates.io plus source install from GitHub. The MCP Registry listing points at\nthe repository metadata; the runtime is the local `signet-eval serve` stdio\nserver.\n\n## Quick Start\n\n**1. Hook into Claude Code** — add to `~/.claude/settings.json`:\n\n```json\n{\n  \"hooks\": {\n    \"PreToolUse\": [{\n      \"matcher\": \"\",\n      \"hooks\": [{\"type\": \"command\", \"command\": \"signet-eval\", \"timeout\": 2000}]\n    }]\n  }\n}\n```\n\nFor Codex, enable hooks in `~/.codex/config.toml` or `<repo>/.codex/config.toml`:\n\n```toml\n[features]\ncodex_hooks = true\n```\n\nThen add `~/.codex/hooks.json` or `<repo>/.codex/hooks.json`:\n\n```json\n{\n  \"hooks\": {\n    \"PreToolUse\": [{\n      \"matcher\": \"*\",\n      \"hooks\": [{\n        \"type\": \"command\",\n        \"command\": \"signet-eval --adapter codex\",\n        \"timeout\": 30000,\n        \"statusMessage\": \"Checking Signet policy\"\n      }]\n    }],\n    \"PermissionRequest\": [{\n      \"matcher\": \"*\",\n      \"hooks\": [{\n        \"type\": \"command\",\n        \"command\": \"signet-eval --adapter codex-permission\",\n        \"timeout\": 30000,\n        \"statusMessage\": \"Checking Signet approval policy\"\n      }]\n    }]\n  }\n}\n```\n\nFor Antigravity, add this block to `~/.gemini/config/hooks.json` (merge it with any existing hook groups):\n\n```json\n{\n  \"signet\": {\n    \"enabled\": true,\n    \"PreToolUse\": [{\n      \"matcher\": \"*\",\n      \"hooks\": [{\n        \"type\": \"command\",\n        \"command\": \"/bin/bash -lc 'source ~/.profile >/dev/null 2>&1 || true; exec signet-eval --adapter antigravity'\",\n        \"timeout\": 30,\n        \"statusMessage\": \"Checking Signet policy\"\n      }]\n    }]\n  }\n}\n```\n\nThe same configuration is available at [`hooks/antigravity-hooks.json`](hooks/antigravity-hooks.json).\n\n**2. Done.** Every tool call now passes through policy evaluation. The default policy blocks destructive operations, protects its own configuration, and allows everything else.\n\n**3. (Optional) Customize** — talk to Claude with the MCP server:\n\n```bash\nclaude mcp add --scope user --transport stdio signet -- signet-eval serve\n```\n\nThen say: *\"Add a $50 limit for amazon orders\"* or *\"Block all rm commands\"*.\n\n## Default Policy\n\nSelf-protection rules are **locked** — they cannot be removed, edited, or reordered by the AI agent, even through the MCP management server. This prevents the agent from disabling its own guardrails.\n\n| Action | Decision | Locked |\n|--------|----------|--------|\n| Write/Edit/Bash touching `.signet/` | **deny** | yes |\n| Write/Edit/Bash touching `signet-eval` binary | **deny** | yes |\n| Write/Edit `settings.json` / `settings.local.json` | **ask** | yes |\n| Bash `kill`/`pkill`/`killall` + `signet` | **deny** | yes |\n| Direct edit tools without recent Kindex tag/search/context | **deny** | yes |\n| Claude `Task*` tools (ephemeral task state) | **deny** | yes |\n| `rm`, `rmdir` | **deny** | |\n| `git push --force` | **ask** | |\n| Git remote and `gh` operations with mismatched target-owner identity | **deny** | |\n| `mkfs`, `format`, `dd if=` | **deny** | |\n| `curl \\| sh`, `wget \\| sh` | **deny** | |\n| Everything else | **allow** | |\n\n## Custom Policy\n\n```bash\nsignet-eval init       # write default policy to ~/.signet/policy.yaml\nsignet-eval validate   # check policy for errors\nsignet-eval rules      # show current rules\n```\n\nEdit `~/.signet/policy.yaml`:\n\n```yaml\nversion: 1\ndefault_action: ALLOW\nrules:\n  - name: block_rm\n    tool_pattern: \".*\"\n    conditions: [\"contains(parameters, 'rm ')\"]\n    action: DENY\n    reason: \"File deletion blocked\"\n\n  - name: books_limit\n    tool_pattern: \".*purchase.*\"\n    conditions:\n      - \"param_eq(category, 'books')\"\n      - \"spend_plus_amount_gt('books', amount, 200)\"\n    action: DENY\n    reason: \"Books spending limit ($200) exceeded\"\n\n  - name: protect_my_config\n    tool_pattern: \".*\"\n    conditions: [\"contains(parameters, '/etc/')\"]\n    action: ASK\n    locked: true\n    reason: \"System config changes require confirmation\"\n```\n\nRules are evaluated in order — first match wins. Multiple conditions on a rule are AND'd. Rules with `locked: true` cannot be modified through the MCP management server.\n\n## Advisory Injection\n\n`INJECT` rules probabilistically add advisory context near the tool call that\ntriggered them. They are nudges, not authorization: the normal\n`ALLOW`/`DENY`/`ASK`/`GATE`/`ENSURE` pass remains first-match-wins and\ndeterministic. Injection runs afterward and only emits context when a matching\ninject rule fires.\n\n```yaml\nrules:\n  - name: maybe_remind_kindex_on_git\n    tool_pattern: \"^Bash$\"\n    conditions: [\"contains(parameters, 'git ')\"]\n    action: INJECT\n    inject:\n      trigger:\n        mode: exponential\n        peak: 0.35\n        cooldown_seconds: 300\n        peak_after_seconds: 1800\n        max_per_session: 3\n      payload:\n        text: \"Before committing, check whether project `.kin` files should be included.\"\n```\n\nTrigger modes:\n\n| Mode | Behavior |\n|------|----------|\n| `constant` / `step` | Fixed probability after cooldown |\n| `linear` | Ramps from 0 to `peak` over `peak_after_seconds` |\n| `exponential` | Approaches `peak` with exponential decay |\n\nPayload sources:\n\n| Source | Notes |\n|--------|-------|\n| `text` | Inline literal text |\n| `text_file` | Bare filename under `~/.signet/injections/` |\n| `from_command` | HMAC-signed allowlist entry from `~/.signet/inject_commands.yaml`; direct exec, no shell |\n\nTemplate substitutions are enabled by default: `{tool_name}`, `{cwd}`, `{date}`,\nand `{matched_param.X}`. See `examples/inject_examples.yaml`.\n\n## Condition Functions\n\n| Function | Description | Example |\n|----------|-------------|---------|\n| `contains(parameters, 'X')` | Tool input contains string | `contains(parameters, 'rm ')` |\n| `any_of(parameters, 'X', 'Y')` | Any string present | `any_of(parameters, 'mkfs', 'format')` |\n| `param_eq(field, 'value')` | Field equals value | `param_eq(category, 'books')` |\n| `param_ne(field, 'value')` | Field not equal | `param_ne(role, 'admin')` |\n| `param_gt(field, N)` | Field > number | `param_gt(amount, 100)` |\n| `param_lt(field, N)` | Field < number | `param_lt(amount, 5)` |\n| `param_contains(field, 'X')` | Field contains substring | `param_contains(command, 'sudo')` |\n| `matches(field, 'regex')` | Field matches regex | `matches(file_path, '\\\\.env$')` |\n| `has_credential('name')` | Credential exists in vault | `has_credential('cc_visa')` |\n| `spend_gt('cat', N)` | Session spend > limit | `spend_gt('books', 200)` |\n| `spend_plus_amount_gt('cat', field, N)` | Spend + this amount > limit | `spend_plus_amount_gt('books', amount, 200)` |\n| `not(condition)` | Negate condition | `not(param_eq(format, 'json'))` |\n| `or(A \\|\\| B)` | Either condition | `or(contains(parameters, '-f') \\|\\| contains(parameters, '--force'))` |\n| `has_recent_action('search', N)` | Recent allowed action matches in tool name or detail; pipe-delimited OR | `has_recent_action('EnterPlanMode\\|TaskCreate', 500)` |\n| `has_current_session()` | Hook host supplied a distinct chat/session identifier | `has_current_session()` |\n| `true` / `false` | Literal | `true` |\n\n## Encrypted Vault\n\nThree-tier encrypted storage with passphrase-derived key hierarchy (Argon2id + AES-256-GCM):\n\n| Tier | Encryption | Contents |\n|------|-----------|----------|\n| 1 | None | Action log, spending ledger |\n| 2 | Session key | Session state |\n| 3 | Compartment key | CC numbers, API tokens, secrets |\n\n```bash\nsignet-eval setup                      # create vault with passphrase\nsignet-eval store cc_visa 4111...      # store Tier 3 credential\nsignet-eval status                     # vault status and spending\nsignet-eval log                        # recent action log\nsignet-eval unlock                     # refresh session after timeout\n```\n\nCredentials support scoped access via `request_capability`: domain restrictions, purpose constraints, per-use amount caps, and one-time tokens that auto-invalidate after a single use.\n\nSpending limits use the vault ledger — each tool call that spends money is logged, and `spend_plus_amount_gt()` checks cumulative totals before allowing the next purchase.\n\n## Self-Protection\n\nsignet-eval ships with locked rules that prevent an AI agent from disabling its own policy enforcement:\n\n1. **protect_signet_dir** — Denies any Write, Edit, or Bash command touching `.signet/` (policy files, vault, HMAC)\n2. **protect_signet_binary** — Denies tampering with the `signet-eval` binary itself\n3. **protect_hook_config** — Requires user confirmation before modifying `settings.json` (where the hook is configured)\n4. **protect_signet_process** — Denies kill/pkill/killall commands targeting signet processes\n\n5. **protect_preflight_storage** — Denies agent-side mutation of preflight records\n6. **require_kindex_engagement_before_edits** — Denies direct edit tools until durable session context is recorded\n7. **prefer_persistent_task_store** — Denies ephemeral `Task*` state and points agents to Kindex tasks\n8. **protect_checks_dir** — Denies agent-side replacement of trusted ENSURE scripts\n9. **protect_vault_passphrase** — Reserves vault setup and unlock operations for the human\n10. **protect_signet_symlink** — Denies symlink bypasses targeting protected enforcement surfaces\n\nThese rules are:\n- **Locked** — MCP tools refuse to remove, edit, or reorder them\n- **Position-protected** — Unlocked rules cannot be reordered above locked rules (first-match-wins)\n- **Hardcoded in defaults** — If the policy file is corrupted or missing, the binary falls back to hardcoded defaults that include self-protection\n- **Version-reconciled** — Current compiled defaults overlay stale system-policy snapshots by rule name, so an upgrade does not require rerunning `init`\n- **Reserved-name reconciled** — Built-in rule names are owned by the binary; host-specific system rules must use distinct names, while user rules remain the supported override layer for unlocked defaults\n- **HMAC-backed** — Direct file edits break the policy signature, triggering fallback to safe defaults\n\n## MCP Management Server\n\nManage policies conversationally through Claude:\n\n```bash\nclaude mcp add --scope user --transport stdio signet -- signet-eval serve\n```\n\n| Tool | Purpose |\n|------|---------|\n| `signet_list_rules` | Show all rules with locked status |\n| `signet_add_rule` | Add a new rule (appended after locked rules) |\n| `signet_remove_rule` | Remove a rule (refuses on locked rules) |\n| `signet_edit_rule` | Modify rule properties (refuses on locked rules) |\n| `signet_reorder_rule` | Move a rule (refuses on locked, prevents placing above locked) |\n| `signet_set_limit` | Set a spending limit for a category |\n| `signet_test` | Test a tool call against the current policy |\n| `signet_validate` | Check policy for errors |\n| `signet_condition_help` | Show available condition functions |\n| `signet_status` | Vault status, spending totals, credential count |\n| `signet_recent_actions` | Show recent action log |\n| `signet_store_credential` | Store a Tier 3 credential |\n| `signet_use_credential` | Request a credential through capability constraints |\n| `signet_list_credentials` | List credential names |\n| `signet_delete_credential` | Delete a credential |\n| `signet_sign_policy` | HMAC-sign the policy file |\n| `signet_reset_session` | Clear spending counters |\n\nAll mutating operations auto-sign the policy when the vault is available.\n\n## MCP Proxy\n\nWrap upstream MCP servers with policy enforcement. The agent connects to the proxy, never directly to servers. Policy is hot-reloaded on every call.\n\n```bash\n# Configure upstream servers\ncat > ~/.signet/proxy.yaml << 'YAML'\nservers:\n  linear:\n    command: npx\n    args: [\"-y\", \"mcp-linear\"]\n    env:\n      LINEAR_API_KEY: \"your-key\"\nYAML\n\n# Register proxy with Claude Code\nclaude mcp add --scope user --transport stdio signet-proxy -- signet-eval proxy\n```\n\n## All Commands\n\n| Command | Purpose |\n|---------|---------|\n| `signet-eval` | Hook evaluation (default, 25ms) |\n| `signet-eval --adapter codex` | Codex `PreToolUse` hook evaluation |\n| `signet-eval --adapter codex-permission` | Codex `PermissionRequest` hook evaluation |\n| `signet-eval --adapter antigravity` | Antigravity `PreToolUse` hook evaluation |\n| `signet-eval init` | Write default policy with locked self-protection rules |\n| `signet-eval rules` | Show current policy rules (locked rules tagged) |\n| `signet-eval validate` | Check policy for errors |\n| `signet-eval test '<json>'` | Test a tool call against policy |\n| `signet-eval setup` | Create encrypted vault |\n| `signet-eval unlock` | Refresh vault session |\n| `signet-eval status` | Vault status and spending |\n| `signet-eval store <name> <value>` | Store Tier 3 credential |\n| `signet-eval delete <name>` | Delete a credential |\n| `signet-eval log` | Recent action log |\n| `signet-eval reset-session` | Clear spending counters |\n| `signet-eval sign` | HMAC-sign policy file |\n| `signet-eval injections` | Show recent inject rule fires |\n| `signet-eval inject-test <rule>` | Force-fire one inject rule for testing |\n| `signet-eval serve` | MCP management server (17 tools) |\n| `signet-eval proxy` | MCP proxy for upstream servers |\n\n## Performance\n\n| Metric | Value |\n|--------|-------|\n| Hook eval (end-to-end) | **25ms** — process spawn, stdin, JSON parse, policy load, eval, response |\n| In-process policy eval | **14–63μs** — 14μs deny, 21μs ask, 63μs spending check |\n| CLI validate / rules | **8ms** |\n| Binary size | **6.2MB** (stripped, LTO) |\n\n## Architecture\n\nsignet-eval is the enforcement layer of the [Signet](https://signet.tools) personal sovereign agent stack. The core principle: **the authorization layer must not be an LLM.** It processes structured data only — regex, comparisons, and vault queries. No natural language, no context window, no persuasion surface. A rule either matches or it doesn't.\n\n```\nAgent proposes action  ->  signet-eval evaluates policy  ->  allow / deny / ask\n                           (deterministic, 25ms, no NLP)\n```\n\n<!-- mcp-name: io.github.jmcentire/signet-eval -->\n\n## Security Model\n\nsignet-eval is a **seatbelt, not a cage**. Understanding what it does and doesn't protect against is critical.\n\n### What it protects against\n\n**A cooperative agent making mistakes.** Claude Code, Codex, and Antigravity follow hook protocols — they call signet-eval around tool use and respect the supported responses. Within those protocols, signet-eval reliably enforces policy:\n\n- Blocks destructive commands (`rm`, `mkfs`, piped remote execution)\n- Enforces spending limits across tool calls\n- Gates credential access with domain, purpose, and amount constraints\n- Requires user confirmation for sensitive operations (`git push --force`)\n- Prevents casual policy tampering via locked rules and MCP guards\n\nThis is the normal operating mode. The agent isn't trying to escape — it's trying to be helpful and the policy keeps it in bounds.\n\nCodex support has one important semantic difference: `PreToolUse` is currently deny-only for enforcement, so Signet `ALLOW` emits no output and Signet `ASK` is converted to a deny at that stage. The `PermissionRequest` adapter explicitly allows or denies requests Codex was already going to send through approval; Signet `ASK` defers to Codex's normal approval prompt.\n\nAntigravity sends a nested `toolCall` payload and expects a top-level `decision` response. The adapter normalizes native command and file tools to Signet's canonical policy names, unwraps `call_mcp_tool` into `mcp__<server>__<tool>`, and fails closed when the native wrapper is malformed.\n\n### What it does NOT protect against\n\n**An adversarial agent with shell access.** signet-eval runs at the same privilege level as the agent it constrains. Any process with the same UID can:\n\n- Use shell indirection to evade string matching (`alias`, `eval`, `base64 -d | sh`, `python3 -c \"...\"`)\n- Create symlinks to protected paths under innocent names\n- Read the session key file and forge HMAC signatures\n- Replace the signet-eval binary itself\n- Modify the hook configuration in `settings.json`\n- Use `LD_PRELOAD`, `ptrace`, or process injection\n\nNo amount of pattern matching fixes this. Adding rules for `ln` invites `alias`. Blocking `alias` invites `eval`. Blocking `eval` invites `python3 -c`. This is an infinite regress — the fundamental issue is privilege level, not pattern coverage.\n\n### Where real containment lives\n\nProtecting against a truly adversarial agent requires **OS-level controls** that operate above the agent's privilege level:\n\n- **Separate user account** — run the agent as a restricted user with no write access to policy files\n- **Container/sandbox** — Docker, Flatpak, or `bubblewrap` with a read-only mount for `~/.signet/`\n- **Stripped `$PATH`** — remove `ln`, `alias`, and other indirection tools from the agent's environment\n- **seccomp / AppArmor / SELinux** — mandatory access controls that the agent cannot override\n- **Virtual environment** — run Claude Code in a constrained environment with limited filesystem access\n\nsignet-eval is the **policy layer** within such a setup. It handles the \"what should this agent be allowed to do\" question with clear, auditable rules. The OS handles the \"can this agent circumvent the policy\" question. Neither replaces the other.\n\n### Defense in depth\n\nThe layers work together:\n\n| Layer | Protects against | Mechanism |\n|-------|-----------------|-----------|\n| **String matching** | Obvious mistakes, clear UX | Regex, substring, word-boundary conditions |\n| **Locked rules** | Casual MCP-based policy tampering | Immutable rules, position protection |\n| **HMAC signing** | Out-of-band file modification | Cryptographic integrity verification |\n| **OS controls** | Privilege escalation, shell indirection | Sandboxing, RBAC, separate users |\n\nWithout OS controls, signet-eval is a speed bump, not a wall. With them, it's the policy engine inside a secure perimeter.\n\n## License\n\nMIT\n",
  "bytes": 18350,
  "sha": "0bdbd4f0577db0caedb543d70e804b1ca5b2950bf2f78c8a6cf8db129e38ce55",
  "repo_slug": "jmcentire/signet-eval",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_jmcentire_signet_eval_68274047/readme"
}