Tenuo for Claude Code
Task-scoped authorization for Claude Code.
Give Claude Code access to your codebase and tools without giving it access to everything. Tenuo constrains what Claude can read, execute, fetch, and invoke before each action runs. You get signed evidence of what was allowed or denied—a tamper-proof audit trail.
Tenuo works alongside Claude Code permissions. It adds a signed, expiring authority token that stays enforced even when permission prompts are bypassed, across MCP calls and subagents.
Why Tenuo?
Claude Code permissions prompt the user; Tenuo checks the model's action at the authorizer boundary. The difference matters:
| Claude Code permissions | Tenuo warrant | |
|---|---|---|
| Trigger | User prompt when Claude asks for access | Automatic check before every tool call |
| Scope | Tool name only | Tool name + argument values |
| Bypass | Prompt can be skipped | Cannot be disabled by the user |
| Evidence | Optional hook logs | Signed, tamper-proof receipts |
| Revocation | Edit rules (active sessions may keep access) | Instant fleet-wide revocation (Cloud) |
| Enforcement | Claude Code settings UI | PreToolUse hook + MCP proxy + authorizer |
| Span | Single session | Persists across MCP calls and subagents |
Tenuo doesn't replace Claude Code permissions—it adds a cryptographic gate that works regardless of why the model tried the call: prompt injection, hallucination, poisoned tool output, or direct user request.
What It Looks Like
After setup, this is what you see:
tenuo-claude audit --verify
DENY Read /etc/passwd constraint not satisfied
path outside workspace (subpath:/home/user/project)
ALLOW Read /home/user/project/main.py constraint satisfied
DENY Bash rm -rf / command not permitted
'rm' not in allowlist [ls,pwd,cat,grep]
3 signed receipts verified:
Ed25519 signatures valid
Hash chain unbroken
Constraints replayed and satisfied
Each decision is:
- Signed with Ed25519 (cryptographic proof)
- Hash-chained (tampering is detected)
- Constraint-replayed (warrant terms verified at audit time)
- Cloud-synced (if configured, sent to tenant for fleet audit)
Quickstart
One command to try it:
uv tool install tenuo-claude-code
tenuo-claude bootstrap --native --pack filesystem-dev
claude
That's it. bootstrap with --pack filesystem-dev generates a safe starter policy (read/write your project, run safe commands), starts the authorizer, and runs a self-test. Open Claude Code, try something in-scope (it works) and out-of-scope (it's denied). Then:
tenuo-claude audit --verify
to see the signed decision log.
What the quickstart does
-
--pack filesystem-devgenerates a preconfigured policy:- Read/write files in your project directory
- Run: ls, pwd, cat, grep (safe commands)
- Deny everything else by default
- Start in
mode: dry-run(log, don't block)
-
--nativeuses the native authorizer binary (no Docker needed) -
bootstrapmints a session warrant, wires the PreToolUse hook, starts the authorizer, and runs a test -
claudeopens Claude Code. The authorizer is now active; every tool call is checked.
After you see it working, edit tenuo.yaml to fit your project, switch to mode: enforce, and run:
tenuo-claude refresh
Available policy packs:
filesystem-dev— Read/write project files, safe commands (default)github-mcp— GitHub MCP server, constrained API querieshttp-api-safe— WebFetch with SSRF protection and domain allowlists
Browse more: examples/policies/.
For Cloud mode (org root, revocation, human approval):
tenuo-claude bootstrap --native --pack filesystem-dev --cloud
Then follow the Cloud setup prompts. Details: Cloud mode.
How It Works
You define what Claude can do in tenuo.yaml:
name: my-project
sandbox: ./workspace
enforce:
Read: "subpath:{sandbox}" # Read only project files
Bash: "shlex:ls,pwd,cat,grep" # Only these commands
WebFetch:
domains: ["api.github.com"] # Only this domain
subagents:
analyst:
tools: [Read, Grep] # Analyst: read-only
default: deny # Block everything else
mode: dry-run # Log, don't block (switch to 'enforce' later)
Tenuo compiles this into a signed, expiring warrant (a capability token). Every Claude action is checked against the warrant before it runs:
- File reads — constrained by
subpath(can't escape the directory) - Shell commands — constrained by
shlex(only allowlisted verbs, no pipes/chaining) - Web fetches — constrained by domain, SSRF-safe
- MCP calls — constrained by tool name and per-argument rules
- Subagent spawns — gated to declared roles, each runs with narrower authority
The same boundary applies no matter why the model tried the call.
Constraints
subpath:DIR— path must resolve inside DIR (symlink-safe)shlex:verb,verb— command executable must be in list; pipes/chaining/subshells denieddomains:[host]— URL domain must match (with SSRF/encoded-IP hardening)oneof:a,b·exact:v·pattern:glob·regex:re·range:min,max— argument must satisfy the constraint- See Policy for the full list and examples
Day-to-Day Commands
tenuo-claude up # Start the authorizer
tenuo-claude refresh # Recompile after editing tenuo.yaml
tenuo-claude audit --verify # Review and verify the decision log
tenuo-claude down # Stop the authorizer
Full command reference: Commands.
Cloud Mode
For organizations: org-root warrants, instant fleet revocation, human approval gates, and a central audit trail.
tenuo-claude bootstrap --native --pack filesystem-dev --cloud
Setup takes ~2 minutes (Cloud tenant, runtime credentials, policy trigger). After that, Tenuo issues warrants signed by your org root, revokes them fleet-wide in ~30s, and sends signed receipts to your tenant for compliance audit.
Human approval is optional: gate any tool to require sign-off (Slack, Telegram, console, etc.) from a named approver.
See Cloud mode for details.
What's in Scope
Tenuo governs model-invoked tool calls on the PreToolUse path:
- Read, Write, Glob, Edit — file access
- Bash, Monitor, PowerShell — command execution
- WebFetch — HTTP requests
- MCP tools — downstream server calls (via proxy)
- Agent — subagent spawning
It does not govern:
- The
!TUI shell (operator command, not model-invoked) - Approved operator actions outside the PreToolUse boundary
Fail-Closed Design
- Missing or broken
tenuo.yamldenies every governed call until restored - Private keys under
.state/are owner-only (0600) - A broken receipt sink surfaces loudly (not silent data loss)
- Cloud-disconnected nodes fall back to local policy (no open bypass)
Policy
tenuo.yaml drives the whole system: warrant, authorizer, hooks, MCP proxy.
name: acme-backend
sandbox: ./workspace # a directory; {sandbox} expands to its absolute path
ttl_seconds: 3600 # optional: session warrant lifetime (default 3600 = 1h)
mode: enforce # block out-of-scope calls; 'dry-run' = log only
enforce:
Read: "subpath:{sandbox}"
Write: "subpath:{sandbox}"
Bash: "shlex:ls,pwd,echo,cat,grep"
WebFetch:
domains: ["api.github.com", "*.githubusercontent.com"]
allow: # permitted, unconstrained (logged)
- TodoRead
default: deny # anything unlisted is denied (recommended, fail-closed)
subagents: # optional: each role runs under a narrower warrant
analyst:
tools: [Read, Grep, Glob]
mcp: # optional: govern a downstream MCP server's tools
downstream: ./your_mcp_server.py
enforce:
read_file: "subpath:{sandbox}"
run_query:
arg: sql
constraint: "regex:^SELECT "
Constraints
| Constraint | Applies to | Checks |
|---|---|---|
subpath:DIR |
Read, Write, Edit, Glob, Grep | Path resolves inside DIR (symlink-safe) |
shlex:a,b,c |
Bash, Monitor, PowerShell | Executable is one of a,b,c; pipes/chaining/subshells denied |
domains:[list] |
WebFetch | URL domain matches allowlist; SSRF/encoded-IP hardening built-in |
oneof:a,b |
any tool | Value is in set |
notoneof:a,b |
any tool | Value not in set |
exact:v |
any tool | Value equals exactly |
pattern:glob |
any tool | Value glob-matches |
regex:re |
any tool | Value regex-matches |
range:min,max |
any tool | Value in numeric range |
urlpattern:url |
any tool | Value matches URL glob |
cidr:n/m |
any tool | Value in IP range |
{sandbox} expands to the directory in sandbox:. You can point subpath: at any path. Tools unlisted aren't individually governed; they're caught by default.
For internal egress, WebFetch also accepts cidrs: (e.g., cidrs: ["10.0.0.0/8"]) to allow by IP range. It's off by default; add deliberately.
Command tools. Bash, PowerShell, and Monitor execute commands independently; govern each in enforce:. Use shlex for POSIX (Bash/Monitor) and pattern/regex for PowerShell (different dialect).
MCP arguments. Under mcp.enforce:, a bare constraint targets path. Use arg: NAME for other arguments, and args: {NAME: constraint, …} for multiple constraints.
Three lists, plus two switches.
enforce:constrained (blocked if constraint fails)allow:unconstrained (always allowed, logged)default:catch-all for unlisted toolsdefault: denyrecommended (fail-closed)default: approveroutes to human approval (Cloud only)
mode:global posturemode: enforceblocks denied callsmode: dry-runlogs decisions, blocks nothing (use to shadow a policy before enforcing)
subagents:declares roles; each runs with attenuated authorityttl_seconds:warrant lifetime in seconds (default 3600 = 1h);upre-mints
After any edit, run tenuo-claude refresh.
Ready-made policies: examples/policies/.
Commands
Day to day: up (start), audit (review), refresh (after editing policy).
| Command | What it does |
|---|---|
onboard |
Interactive first-run wizard (--local / --cloud); same as bootstrap but with prompts. |
bootstrap |
Non-interactive setup: scaffold policy if none → init → up → verify. --cloud for Cloud mode; --pack PACK to use a policy pack. |
init |
Compile existing tenuo.yaml: mint warrant, wire hook and MCP proxy. |
up / down |
Start / stop the authorizer (auto-selects Docker or native; --native forces native). |
refresh |
Recompile after editing tenuo.yaml (restarts authorizer if running). |
verify [--deep] |
Self-test the policy against the authorizer (no Claude session needed). --deep adds SSRF/encoded-IP matrix and Bash deny cases. |
audit [--tail N] [--verify] |
Show the decision log (.state/receipts.jsonl). --verify checks receipt signatures, hash-chain links, and embedded evidence. |
check |
Preflight: dependencies, wiring, audit-sink health, leaked keys, Cloud bindings. |
status |
Warrant, mode, audit-sink health, Cloud summary. |
install-authorizer |
Install the pinned native authorizer to ~/.tenuo/bin. |
bench [--json] |
Measure per-call overhead on your machine. |
revoke |
Revoke the current session warrant. |
The warrant is short-lived (~1h TTL); up refreshes it before expiry. Generated files (don't commit): .state/ (keys, warrant, credentials), .claude/settings.json (hooks), .mcp.json (MCP proxy).
Cloud Mode
For organizations: org root, fleet revocation, human approval, central audit.
Setup (first time)
-
Create a Tenuo tenant at cloud.tenuo.ai. Complete the Infrastructure onboarding (provisions KMS-backed signing keys).
-
Get credentials:
- Runtime key (
tenuo_ct_…) from Agents → Quick Connect → Authorizer Only →.state/cloud.env - Tenant-admin key (
tc_…) from Settings → API Keys → Create (admin role) →~/.tenuo/admin.env
- Runtime key (
-
Bootstrap with Cloud:
tenuo-claude bootstrap --native --pack filesystem-dev --cloud
Behind the scenes:
tenuo-admin setupregisters this project, creates a Cloud trigger fromtenuo.yaml, and mints a root-signed warrant. -
(Optional) Add human approval:
- Create an approver identity in Cloud (Dashboard → Channels → Identity Bindings)
- Give it a notification channel (Slack, Telegram, console, etc.)
- Gate any tool with
approval:intenuo.yaml
Day to day (Cloud)
tenuo-claude check && tenuo-claude up
If check reports a Cloud bindings failure, run tenuo-admin setup and retry.
After editing tenuo.yaml:
tenuo-claude refresh
The Cloud-issued warrant stays active until revoked (or TTL expires). To revoke fleet-wide:
tenuo-claude revoke
~30 seconds later, all active sessions enforcing that warrant exit and must re-authorize.
Important: Once Cloud is configured, don't re-run plain bootstrap (it reverts to local mode). Use check && up on future sessions.
Human Approval (Cloud)
Add approval: to any enforced tool:
enforce:
WebFetch:
domains: ["api.github.com"]
approval: true # all URLs gated
delete_deployment: # MCP tool
arg: target
constraint: "oneof:staging,prod"
approval:
exempt: ["staging"] # staging bypasses approval
When Claude tries a gated action:
- The hook opens a Cloud approval request
- The approver gets a Slack/Telegram/console notification
- They review and approve/deny
- The decision is recorded in the signed receipt (audit trail shows who approved)
Setup: See docs/DETAILS.md § Approval setup runbook.
For Cloud, receipts are sent to your tenant (central audit). Locally, receipts are always written to .state/receipts.jsonl.
Security Model
Tenuo runs alongside Claude Code permissions; it doesn't replace them. The difference:
- Claude Code permissions: Prompt the user; can be bypassed with
--dangerously-skip-permissions - Tenuo warrant: Signed, expiring authority enforced by an authorizer before each call; cannot be disabled by the user or model
The warrant is a capability token: it lists what Claude can do and for how long. Each action is checked against the warrant before it runs. The same boundary applies regardless of why the model tried the call: prompt injection, hallucination, poisoned tool output, or direct user request.
Receipts: Every decision is signed with Ed25519 and hash-chained for tamper-evidence. Local audit --verify checks signatures, hash-chain links, warrant chains, and deterministic constraint replay. Cloud receipts are independently verified by the tenant.
Fail-closed: Missing or broken tenuo.yaml denies every governed call. Keys under .state/ are owner-only. A broken receipt sink surfaces loudly (not silent data loss).
Rolling out to a team: Keep tenuo.yaml in version control. Push hook/MCP wiring through Claude Code managed settings. Use Cloud for org-root warrants, central audit, and revocation. See docs/DETAILS.md § Managed deployment for the rollout checklist and hardened socket setup.
Turning it off: tenuo-claude disable unwires the hook (Claude stops calling the authorizer) and stops the authorizer, leaving policy and warrant in place. tenuo-claude uninstall also deletes .state/ (warrant, keys, credentials). tenuo.yaml is never touched.
Build from Source
For development or running from a git checkout:
git clone https://github.com/tenuo-ai/tenuo-claude-code.git
cd tenuo-claude-code
uv venv && uv sync && source .venv/bin/activate # Windows: .venv\Scripts\activate
chmod +x bin/tenuo-claude
uv run tenuo-claude install-authorizer # only if not using Docker
Run via ./bin/tenuo-claude --help, uv run tenuo-claude --help, or pip install -e .. Re-run tenuo-claude init (or refresh) after moving the repo or reinstalling.
Contributors: CONTRIBUTING.md.
Performance
Authorization is ~1–3 ms per call; the PreToolUse hook adds ~100–200 ms (mostly process startup). Measure on your machine:
tenuo-claude bench
About
Repository: tenuo-ai/tenuo-claude-code
PyPI: tenuo-claude-code
Tenuo core: tenuo-ai/tenuo
License: Apache 2.0
| Path | |
|---|---|
| Package source | src/tenuo_claude_code/ |
| Policy packs | examples/policies/ |
| Reference demo | demo/ |
| Implementation details | docs/DETAILS.md |
| Troubleshooting | docs/TROUBLESHOOTING.md |
| Security | SECURITY.md |
Release files for tenuo-claude-code 0.6.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tenuo_claude_code-0.6.4.tar.gz | 627.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tenuo_claude_code-0.6.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 740.8 kB
Release files / tenuo_claude_code-0.6.4.tar.gz
| Download URL | tenuo_claude_code-0.6.4.tar.gz |
|---|---|
| Size | 627.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0cfd81e7c5377443520694ea430e87d9503c66579da4b81bd43d7c637f4eb3fd
|
|
BLAKE2b-256 checksum How to use checksums |
27d27e436c636d5f6aeb06a711d1f1e8447fe7aa70a1fab3400089f49f6c871c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / tenuo_claude_code-0.6.4-py3-none-any.whl
| Download URL | tenuo_claude_code-0.6.4-py3-none-any.whl |
|---|---|
| Size | 113.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ea9a31290c0ce79a4318088b4fbe45ef1e2fd2e079d6b27ea3a9490464d25654
|
|
BLAKE2b-256 checksum How to use checksums |
fc1a1c844cbf955a79cda031c19234bb9e9aca1ceeaef4bcaf52e8b2c49314ce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|