Tenuo governance for Claude Code — warrants, hooks, MCP proxy, and Cloud lifecycle
Project description
Tenuo for Claude Code
Keep your AI coding agent inside the lines. Claude Code can read files, run shell, fetch URLs, and call MCP tools. Tenuo puts a signed policy in front of all of it so the agent only does what you allowed, on one laptop or across a Cloud-connected fleet.
Why: Claude's permission prompts are easy to bypass (--dangerously-skip-permissions, local settings edits). Hook-only guardrails trust the process. Tenuo adds limits you can prove were enforced: sandbox-scoped reads, shell allowlists, MCP argument checks, URL policy, and an audit trail per call.
What you get: one policy file (tenuo.yaml), a local authorizer that says allow/deny on every call, and optional Tenuo Cloud for verifiable receipts, human approval gates on high-risk calls, and fleet-wide revocation.
Try it
Requires Python 3.10+ and Docker or a native authorizer.
pip install tenuo-claude-code
mkdir my-project && cd my-project
tenuo-claude bootstrap
Open Claude Code here when verify passes. No Claude? bootstrap + verify is enough to prove enforcement.
No Docker, or prefer native? Run tenuo-claude install-authorizer once, then bootstrap. Without Docker, the CLI uses native automatically. To force native while Docker is running, set TENUO_AUTHORIZER_BACKEND=native before bootstrap.
Where to go next
| If you want to… | Start here |
|---|---|
| Day-to-day commands, ports, CLI reference | Use the tool |
| Edit or replace the example policy | Policy |
| Clone, hack, or run the sample project | Build from source |
| Connect to Tenuo Cloud | Cloud mode |
| Review security posture | Security |
| Plan org-wide rollout | Talk to us |
| Implementation depth | docs/DETAILS.md |
How it works
tenuo.yaml → init/up → warrant + authorizer + hooks + MCP proxy
|
|
v
native tools (PreToolUse hook) | MCP tools (proxy)
|
|
v
authorizer → allow / deny → log / receipt
Native tools (Read, Bash, WebFetch, and the rest) go through a PreToolUse hook. MCP tools go through a proxy in place of the downstream server. At init, Tenuo mints a session warrant; each call must prove it holds that warrant before the authorizer allows the action.
Both paths use the same warrant and authorizer.
More: docs/DETAILS.md
Prerequisites
-
Python ≥ 3.10
-
Authorizer runtime (pick one):
- Docker: Docker Desktop or your engine, then
tenuo-claude up. Pulls the pinnedtenuo/authorizerimage. Default when Docker is running. - Native (no Docker):
tenuo-claude install-authorizer, thentenuo-claude up --native. First run: add--install. Override the binary withTENUO_AUTHORIZER_BIN. SetTENUO_AUTHORIZER_SKIP_VERSION=1only for local dev builds.
Without Docker,
bootstrapuses native. Runinstall-authorizerfirst. To force native while Docker is running, setTENUO_AUTHORIZER_BACKEND=native. - Docker: Docker Desktop or your engine, then
-
Claude Code for live agent use. Optional:
verifyworks without it.
Local mode: no Tenuo Cloud account. Good for one project or evaluation.
Cloud mode: cloud.tenuo.ai tenant for tenant-root warrants, central audit, fleet revocation, and org-wide rollout (Cloud mode).
Use the tool (PyPI)
After Try it, you have an example tenuo.yaml, a running authorizer, and passing verify. Stay in that project directory for every command below.
Day to day
| When | Command |
|---|---|
| Start work | tenuo-claude up or tenuo-claude up --native |
You edited tenuo.yaml |
tenuo-claude refresh |
| Something broken | tenuo-claude check |
| See decisions | tenuo-claude audit |
| Stop authorizer | tenuo-claude down |
If you always use native without Docker, set TENUO_AUTHORIZER_BACKEND=native in your shell so plain up picks the host binary.
Port conflicts
The authorizer listens on 127.0.0.1:9090 by default. If that port is taken, set TENUO_AUTHORIZER_PORT before bootstrap:
export TENUO_AUTHORIZER_PORT=9091
tenuo-claude bootstrap
PORT is deprecated. Use TENUO_AUTHORIZER_PORT.
The chosen URL is saved in .state/state.json. Hooks and verify read that file, so they stay aligned even if the env var is unset later.
Generated files (do not commit): .state/ (keys, warrant), .claude/settings.json (hooks).
Custom policy
Bring your own tenuo.yaml (see Policy), or edit the example bootstrap wrote, then:
tenuo-claude init
tenuo-claude up
tenuo-claude verify
Use --native when not running Docker. First native run: add --install. Or run tenuo-claude onboard --local.
Reference demo
Sample policy, MCP stub, and scripted tour in demo/. The shipped demo/tenuo.yaml is richer than the bootstrap example; Cloud and approval overlays may already be present.
First time, local only (no .state/cloud.env yet):
cd demo
tenuo-claude bootstrap
tenuo-claude demo
Already Cloud-configured (.state/cloud.env or tenuo.cloud.yaml present): do not run plain bootstrap. It switches to local mode and moves Cloud files aside. Use:
cd demo
tenuo-claude up
tenuo-claude verify
tenuo-claude demo
For human approval: Cloud mode § Human approval, then tenuo-claude demo --advanced --live-approval.
If you ran local bootstrap on a Cloud setup by mistake, restore .state/cloud.env and overlays from backup, then run tenuo-admin setup to re-claim the holder key.
From a git checkout, see Build from source.
All commands
| Command | Does |
|---|---|
init |
Mint warrant, wire hooks and .mcp.json |
bootstrap |
Example policy (if missing) + check + init + up + verify |
up / down |
Start / stop authorizer. up flags: --native, --docker, --install (native, first run) |
install-authorizer |
Install tenuo-authorizer to ~/.tenuo/bin (no manual cargo) |
refresh |
Re-apply tenuo.yaml (restarts authorizer if up) |
check |
Preflight: deps, credentials, wiring drift |
verify [--deep] |
Policy self-test against the authorizer |
status |
Warrant, posture, Cloud summary |
onboard |
Interactive local or Cloud setup wizard |
bench [--json] |
Per-tool-call overhead |
audit [--tail N] |
Receipt trail |
revoke |
Revoke session warrant |
See also: Policy · Cloud mode · docs/DETAILS.md
Build from source
For hacking on the CLI, running the reference demo from git, or using ./bin/tenuo-claude instead of a PyPI install.
git clone https://github.com/tenuo-ai/claude-governance.git
cd claude-governance
uv venv && uv sync && chmod +x bin/tenuo-claude
source .venv/bin/activate # Windows: .venv\Scripts\activate
Without Docker, install the authorizer binary once: uv run tenuo-claude install-authorizer
Run commands via the repo launcher or editable install:
./bin/tenuo-claude --help
# or: uv run tenuo-claude --help
# or: pip install -e . && tenuo-claude --help
Reference demo (from a git checkout):
cd demo
tenuo-claude up # if Cloud is already wired; see Reference demo above
tenuo-claude demo
First local-only run: tenuo-claude bootstrap instead of up, only when .state/cloud.env is absent.
Use tenuo-claude up --native instead of plain up if you are not running Docker.
Open Claude Code in demo/. See demo/README.md and Reference demo above.
Re-run tenuo-claude init or refresh after switching Python venvs. Hooks pin sys.executable in .claude/settings.json.
Contributors: CONTRIBUTING.md.
Policy (tenuo.yaml)
One file drives the warrant, authorizer routes, hooks, and MCP proxy. bootstrap writes a minimal example and sets name: from your folder name. Edit before production.
name: acme-backend # bootstrap uses your project directory name
sandbox: ./workspace
mode: enforce
enforce:
Read: "subpath:{sandbox}"
Bash: "shlex:ls,pwd,echo,date"
WebFetch:
domains: ["api.github.com", "*.githubusercontent.com"]
default: deny
subagents:
analyst:
tools: [Read, Grep, Glob]
mcp:
downstream: ./your_mcp_server.py
enforce:
read_file: "subpath:{sandbox}"
enforce: allowed and argument-checked.audit: harness tools from bundled list (extend withaudit_extra:).default: deny: everything else blocked with a receipt.mode: audit: receipt allow/deny without blocking (rollout).subagents:: DETAILS.md.
Policy templates: ready-made patterns in examples/policies/. Download one, save as tenuo.yaml in your project directory, adapt sandbox paths and tool lists, then run tenuo-claude init. To contribute a template, see that folder's README.
Cloud overlays: templates/tenuo.yaml.cloud.example, templates/tenuo.yaml.advanced.example. Download from this repo or use tenuo-claude init --cloud / --advanced.
Cloud mode
Requires a cloud.tenuo.ai tenant.
Use Cloud when you need organization-scale governance, not just a single laptop:
- Tenant-root warrants: session credentials chain to your tenant, not a local issuer key
- Central audit: signed allow/deny/spawn/approval receipts in one stream
- Fleet revocation: revoke a warrant id; authorizers pull the SRL within ~30s
- Split keys: admins run
tenuo-admin setuponce; developers use runtime keys only - Optional approver gates: high-risk governed tool calls can pause for Cloud identity sign-off before proceeding (setup)
- Managed rollout: wire hooks via Claude Code managed settings; keep team
tenuo.yamlin git - Org-wide baseline: with managed settings, the signed warrant is enforced on
every tool call at the hook and MCP proxy. Engineers cannot disable it with local
Claude permission edits or
--dangerously-skip-permissions
Local mode (no Cloud account) remains fully supported for evaluation and single-project use.
Cloud quickstart
Two keys, two files. Runtime never sees the admin key:
| Key | Where to get it | File | Used by |
|---|---|---|---|
| Quick Connect (runtime) | cloud.tenuo.ai → Agents → Quick Connect → Authorizer Only → copy tenuo_ct_… |
.state/cloud.env |
tenuo-claude up, hooks |
| Tenant-admin (setup once) | Settings → API Keys → Create (tenant admin role) | ~/.tenuo/admin.env |
tenuo-admin setup once |
Do not put the tenant-admin key in .state/cloud.env or your shell when running tenuo-claude up. Runtime refuses to start if an admin key is reachable.
Fresh project
Creates an example tenuo.yaml if none exists.
pip install tenuo-claude-code
mkdir my-project && cd my-project
tenuo-claude bootstrap --cloud
Requires a Quick Connect token (tenuo_ct_…). The wizard prompts for it. Using explicit Cloud URL + API key instead? Use the manual setup block below; bootstrap --cloud expects Quick Connect.
Prompts for credentials, then runs init, tenuo-admin setup, up, and verify. Pass a tenant-admin key to run setup in the same step.
Non-interactive:
tenuo-claude bootstrap --cloud --yes \
--connect-token "tenuo_ct_…" \
--admin-key "tc_…"
Omit --admin-key if tenuo-admin setup already ran. Prefer flags over export TENUO_ADMIN_KEY. A lingering admin key in the shell makes later tenuo-claude up fail.
One-shot env vars for CI:
TENUO_CONNECT_TOKEN="tenuo_ct_…" TENUO_ADMIN_KEY="tc_…" \
tenuo-claude bootstrap --cloud --yes
Existing local project
Keeps your current tenuo.yaml. Adds Cloud credentials and profile, re-mints the session warrant, and registers or updates the Cloud trigger.
cd my-project
tenuo-claude onboard --cloud
Without providing an admin key to the wizard, have an admin run tenuo-admin setup before up, or place the admin key in ~/.tenuo/admin.env and run it yourself.
Manual equivalent. Credential templates in templates/:
cd my-project
mkdir -p .state ~/.tenuo
# templates/cloud.env.example → .state/cloud.env
# templates/admin.env.example → ~/.tenuo/admin.env
tenuo-claude init --cloud
tenuo-admin setup
tenuo-claude up
tenuo-claude verify
Success looks like
After onboarding:
tenuo-claude status
tenuo-claude check
tenuo-admin show
You should see: authorizer up, cloud profile merged, a trigger id in tenuo-admin show, and CHECK OK with no admin key in runtime env.
Re-run tenuo-admin setup when Cloud capabilities change — setup syncs the local
gateway and reloads the authorizer when it is already running. Re-run
tenuo-claude refresh for local-only policy edits (no Cloud trigger change).
Human approval (Cloud)
Approval is a third outcome on any governed tool call: not just egress.
When the warrant includes an approval gate for a capability, the authorizer can return approval-required instead of allow/deny. The hook creates a Cloud approval request, waits for an approver on their notification channel, then re-authorizes with signed approvals in X-Tenuo-Approvals.
Included policy examples:
- Native hook: off-allowlist
WebFetch(URLs that pass SSRF checks but are not on your domain allowlist). - MCP proxy:
delete_deploymentwithtarget=productiongated;target=stagingexempt.
Both use the same session approval policy and the same hook/proxy approval workflow for any tool argument the warrant gates.
- Configure a notification channel and identity binding in Cloud (channels, identity bindings).
- Add approval gates in policy. Prefer
cloud.approver_identity_idfor team/shared configs;cloud.approver_identity(display name) is fine for demos and quickstarts. See templates/tenuo.yaml.advanced.example. - Wire Cloud and re-run setup:
tenuo-claude init --advanced --approver-id "idn_..."
tenuo-admin setup # syncs gateway; reloads authorizer if already up
tenuo-claude up # if authorizer was down
tenuo-claude verify
For a quick demo, display-name lookup is still supported:
tenuo-claude onboard --cloud --advanced --approver "Alice Example"
If multiple Cloud identities share a display name, setup will ask you to use
--approver-id / cloud.approver_identity_id.
Reference demo:
cd demo
tenuo-claude demo --advanced --live-approval
For the WebFetch example: allowlisted domains pass directly. SSRF cases remain hard-denied. For the MCP example: exempt targets pass directly; other argument values pause for sign-off. Both paths use the same Cloud approval request flow.
Details: docs/DETAILS.md § Human approval.
Security
Tenuo works alongside Claude Code permissions. It does not replace managed settings.
You still deploy hooks. Tenuo adds a signed session warrant, a local authorizer on every tool call, and a decision log per call. Policy is one file (tenuo.yaml).
vs. Claude Code permissions
| Claude Code permissions | Tenuo warrant | |
|---|---|---|
| Policy | Allow/ask/deny in settings | Signed credential; Cloud chains to tenant root |
| Expiry | Until edited | Session TTL (~1h); up refreshes |
| Revocation | Edit rules; sessions may keep prior allowances | Revoke warrant id → ~30s SRL sync (Cloud) |
| Evidence | Optional hook logs | Local JSONL log; signed audit stream in Cloud |
| Delegation | Project/user tool policy | Per-role child warrant; session is the ceiling |
| Org-wide deployment | Per-user settings; users can edit local hooks | Managed settings + shared policy; hook/proxy enforcement is not user-editable |
--dangerously-skip-permissions |
Bypasses Claude permission prompts | Hook and MCP proxy still enforce the warrant |
That flag skips Claude's permission UI, not the warrant.
Org admins can block it in managed settings (disableBypassPermissionsMode).
Organization-wide policy
For teams that need a global configuration engineers cannot bypass:
- Store
tenuo.yamlin version control and deploy the same policy to every project. - Push hook and MCP wiring through Claude Code managed settings (org admin), not
per-developer
settings.local.json. - Use Cloud for tenant-root warrants, central audit, and revocation across machines.
Talk to us about managed-settings rollout and Cloud deployment.
Receipts
Every governed tool call must prove possession of the session key (proof-of-possession) when asking the authorizer for allow/deny.
What you can read back depends on mode:
Local (always): the hook appends a JSON line to .state/receipts.jsonl. Inspect
with tenuo-claude audit:
{"phase": "pre", "decision": "deny", "claude_tool": "Read", "governed": true,
"args": {"file_path": "/etc/passwd"}, "reason": "Constraint not satisfied", "ts": "…"}
In mode: audit, denials show as WOULD-DENY in audit output (shadow: true in the file).
Cloud (when connected): the authorizer emits signed audit events (Ed25519 over a CBOR payload) to your tenant.
These are the non-repudiable receipts for compliance and fleet audit, not the local JSONL file.
More: docs/DETAILS.md § Receipts.
Cloud audit
With Tenuo Cloud, session warrants chain to your tenant root.
Allow, deny, spawn, and approved exceptions appear in one signed audit log. Revoke a warrant id from status or the dashboard without touching the laptop.
Admin and runtime keys stay split: tenuo-admin setup (once) vs tenuo-claude up (daily).
Runtime refuses to start if an admin key is in the environment.
Rollout
- Start on one project with Try it or the demo/ sample.
- Set
mode: auditto compute allow/deny in receipts without blocking. ReviewWOULD-DENYrows, tune policy, then setmode: enforce. - Organization-wide rollout: managed-settings hooks, Cloud warrants, shared
tenuo.yamlin version control, and the Cloud capabilities above. See Tenuo Cloud docs.
Scope and fail-closed
Governance covers agent tool calls (Read, Bash, MCP, subagent spawns), not interactive ! shell in the Claude TUI (Map vs Territory).
Missing or broken tenuo.yaml denies every call until restored.
Keys and credentials in .state/ must be owner-only (0600 in a 0700 directory).
The authorizer reads gateway config from .state/authorizer/ (Docker mount or native process). Holder keys and cloud.env stay on the host. Session-key signing runs in the hook.
Report vulnerabilities: SECURITY.md.
Implementation depth: docs/DETAILS.md.
Performance
Run tenuo-claude bench after up.
On a typical laptop, Tenuo authorization is about 1-3 ms per call. Command hooks add about 100-200 ms (mostly process startup). Use bench --json on your machines.
This repo
GitHub: tenuo-ai/claude-governance
PyPI: tenuo-claude-code
| Path | Contents |
|---|---|
src/tenuo_claude_code/ |
Package source |
templates/ |
Starter tenuo.yaml and credential examples |
examples/policies/ |
Ready-made policy templates |
demo/ |
Reference project and scripted tour |
docs/ |
Architecture diagram, implementation notes |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tenuo_claude_code-0.2.1.tar.gz.
File metadata
- Download URL: tenuo_claude_code-0.2.1.tar.gz
- Upload date:
- Size: 536.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2611aeb90646e517a2739cb1a51ae0539eb84c05959c38176236cee3a037ea14
|
|
| MD5 |
243a7851f3695b85ec406d2b4370f255
|
|
| BLAKE2b-256 |
845f0a737d4c42b9cf1a8395baeb926d1f594a915d17482d97ef29783c03e1a4
|
File details
Details for the file tenuo_claude_code-0.2.1-py3-none-any.whl.
File metadata
- Download URL: tenuo_claude_code-0.2.1-py3-none-any.whl
- Upload date:
- Size: 67.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f096359666a789e8912a07db384404e5eccd2f317fe4fcf3eabb917c661b1271
|
|
| MD5 |
ff9a06b255a3ed2eced8ff2fbef80991
|
|
| BLAKE2b-256 |
c6e2f481860f3cd6b813fcbebc75761e00c7c6f56281059486bb3b46b46de7bb
|