Skip to main content

Tenuo for Claude Code

PyPI Python CI License: Apache-2.0

Claude Code's permissions decide which tools Claude can call. Tenuo decides what it can call them with.

Tenuo checks the arguments of every tool call against your policy before the call runs: which repo, which directory, which commands, which domains. That includes MCP tools, with no change to the MCP server. Each decision is written as a signed receipt.

Here Claude Code's own permissions allowed the GitHub MCP server's get_file_contents. The Tenuo policy allows one repository:

mcp:
  server: github             # must match the client MCP server name
  enforce:
    get_file_contents:
      args:
        owner: "exact:tenuo-ai"
        repo: "pattern:tenuo"
default: deny
$ tenuo-claude audit
  ALLOW      [gov] mcp__github__get_file_contents -> mcp__github__get_file_contents  authorized  owner=tenuo-ai path=README.md repo=tenuo
  DENY       [gov] mcp__github__get_file_contents -> mcp__github__get_file_contents  Constraint not satisfied  owner=tenuo-ai path=README.md repo=tenuo-cloud
  DENY       [gov] mcp__github__get_file_contents -> mcp__github__get_file_contents  Constraint not satisfied  owner=torvalds path=README repo=linux

The same check covers Claude's own tools: reads and edits stay inside the project, the shell runs only the commands you list, and WebFetch reaches only the domains you name.

Why Tenuo?

Claude Code permissions Tenuo
Matches Tool name; command and path patterns for built-in tools Any argument of any tool, including MCP tools
Decides Allow, deny or ask the user Allow or deny before the call, against a signed, expiring warrant
Evidence Optional hook logs Signed, hash-chained receipts (tenuo-claude audit --verify)
Subagents Same rules as the session Each declared subagent gets a narrower warrant
Teams Managed settings Managed settings plus an org-signed root, revocation and approvals (Cloud)

Tenuo runs alongside Claude Code permissions. It checks the call whatever led the model to make it: prompt injection, a poisoned tool result, or a plain mistake.

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 starter policy (read and edit your project, keep Tenuo's keys and hook wiring out of reach, a minimal shell), starts the authorizer, and runs a self-test. The first run also downloads the pinned authorizer binary to ~/.tenuo/bin. The pack starts in mode: dry-run, so nothing is blocked yet: open Claude Code, work normally, ask for something out of scope (say, reading a file outside the project), then:

tenuo-claude audit --verify

to see the signed decision log, including what would have been denied.

What the quickstart does

  1. --pack filesystem-dev generates a preconfigured policy:

    • Read, edit and search files in your project directory (paths resolved, symlinks followed)
    • Never read or change Tenuo's own files: its keys and state, tenuo.yaml, .mcp.json, .claude/settings*.json
    • Run only ls and pwd in the shell; add your test and build commands to Bash:. The shell check covers the command name, not its paths, so list only commands you'd allow on any path
    • Deny everything else by default
    • Start in mode: dry-run (log, don't block)
  2. --native uses the native authorizer binary (no Docker needed)

  3. bootstrap mints a session warrant, wires the PreToolUse hook, starts the authorizer, and runs a test

  4. claude opens 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, minimal shell (default)
  • github-mcp — GitHub MCP server, constrained API queries
  • http-api-safe — WebFetch with SSRF protection and domain allowlists

Browse more: examples/policies/.

Limit an MCP server

Add the server to Claude Code as usual, then scope it. For the GitHub MCP server:

tenuo-claude init --pack github-mcp --param owner=myorg --param repos='service-*' --param approver=any-human

The pack starts in mode: dry-run, which logs decisions but does not block calls. Review the generated tenuo.yaml, change it to mode: enforce, then apply it and start the authorizer:

tenuo-claude refresh
tenuo-claude up

In enforce mode, reads and ordinary writes are limited to myorg/service-*; merges, pushes and file writes need a human approval (Tenuo Cloud; locally they are denied); everything else is denied. For another server, list its tools under mcp.enforce with a constraint per argument; see Policy.

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.

Install as a Claude Code plugin

The repo is also a Claude Code plugin marketplace. Inside Claude Code:

/plugin marketplace add tenuo-ai/tenuo-claude-code
/plugin install tenuo@tenuo

The plugin still needs the CLI (uv tool install tenuo-claude-code) and a policy (tenuo-claude bootstrap --native --pack filesystem-dev). What it adds:

  • Hooks that stay out of the way. PreToolUse, PostToolUse, and SessionStart hooks run in every project, but they do nothing unless the project has opted in. Repos without Tenuo keep Claude Code's normal permissions.
  • A fail-closed safety net. If a project is governed but its .claude/settings.json wiring is missing (a fresh clone, a regenerated settings file), the plugin routes each call to tenuo-claude _hook itself. If the CLI can't be found or crashes, the call is denied with a message saying how to fix it.
  • A one-line status at session start, plus /tenuo:status (tenuo-claude check) and /tenuo:audit (tenuo-claude audit --verify).

How the plugin decides, per tool call:

Project state PreToolUse result
No tenuo.yaml in the session directory or any parent Nothing (Claude Code permissions apply)
tenuo.yaml but no state directory (never initialized) Nothing; session start says to run bootstrap
<state>/disabled present (tenuo-claude disable) Nothing; tenuo-claude up or init re-arms it
Tenuo hooks already wired by init/bootstrap (project, user, or managed settings) Nothing; the wired hook enforces, so no double receipts
Initialized, not wired, tenuo-claude found Delegates to tenuo-claude _hook
Initialized, not wired, tenuo-claude missing, crashing, or timing out Deny (fail-closed)
TENUO_PROJECT_DIR set to a directory without tenuo.yaml Deny (fail-closed)

The CLI is found via TENUO_CLAUDE_BIN, the project .venv, PATH, then the uv tool and pipx install locations. The launcher is stdlib Python (plugins/tenuo/scripts/tenuo_plugin.py); it makes no network calls itself. macOS and Linux only, like the CLI.

To try a local checkout: claude --plugin-dir ./plugins/tenuo.

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 denied
  • domains:[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.yaml denies every governed call until restored
  • Private keys are owner-only (0600) and kept outside the project
  • In enforce mode a recursive Grep runs with the policy and hook wiring files (tenuo.yaml, .mcp.json, .claude/settings*.json) excluded, and is denied if they can't be excluded. Keep secrets out of those files anyway (${VAR} references), since dry-run blocks nothing
  • 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
  server: tenuo-files        # client MCP server name (default: tenuo-files)
  downstream: ./your_mcp_server.py   # or {command: npx, args: [...], env: {...}}
  enforce:
    read_file: "subpath:{sandbox}"
    run_query:
      arg: database
      constraint: "oneof:analytics,staging"

MCP capabilities include the server name: mcp__<server>__<tool>. A different server with the same tool name does not inherit these grants. For upgrades, see 0.7.0 migration notes.

Any MCP client

tenuo-claude mcp-proxy serves mcp.downstream over stdio and checks each call against the policy before forwarding it, so the same rules apply in Cursor, Claude Desktop or any other MCP client. The downstream can be any stdio server. Env values can reference the proxy's environment, which keeps tokens out of tenuo.yaml:

mcp:
  server: github
  downstream:
    command: docker
    args: [run, -i, --rm, -e, GITHUB_PERSONAL_ACCESS_TOKEN, ghcr.io/github/github-mcp-server]
    env:
      GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}"
  enforce:
    get_file_contents:
      args: {owner: "exact:myorg", repo: "pattern:service-*"}
default: deny

Then point the client at the proxy instead of the server, with tenuo-claude up running in the project:

{
  "mcpServers": {
    "github": {
      "command": "tenuo-claude",
      "args": ["mcp-proxy"],
      "env": {"TENUO_PROJECT_DIR": "/path/to/project", "GITHUB_TOKEN": "..."}
    }
  }
}

A denied call returns an MCP error result (isError: true) naming the refused constraint, and is recorded in tenuo-claude audit.

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 tools
    • default: deny recommended (fail-closed)
    • default: approve routes to human approval (Cloud only)
  • mode: global posture
    • mode: enforce blocks denied calls
    • mode: dry-run logs decisions, blocks nothing (use to shadow a policy before enforcing)
  • subagents: declares roles; each runs with attenuated authority
  • ttl_seconds: warrant lifetime in seconds (default 3600 = 1h); up re-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.
state-dir Print this project's state directory.
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): .claude/settings.json (hooks), .mcp.json (MCP proxy).

State. Keys, warrant, gateway config, receipts and Cloud credentials live outside the project, in ~/.tenuo/projects/<project>-<hash>/, so nothing scoped to the project tree can reach them. tenuo-claude state-dir prints the path; <state>/… in these docs means a file in it. A project set up by an older release keeps using <project>/.state until the next tenuo-claude refresh, which moves it.

Cloud Mode

For organizations: org root, fleet revocation, human approval, central audit.

Setup (first time)

  1. Create a Tenuo tenant at cloud.tenuo.ai. Complete the Infrastructure onboarding (provisions KMS-backed signing keys).

  2. 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
  3. Bootstrap with Cloud:

    tenuo-claude bootstrap --native --pack filesystem-dev --cloud
    

    Behind the scenes: tenuo-admin setup registers this project, creates a Cloud trigger from tenuo.yaml, and mints a root-signed warrant.

  4. (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: in tenuo.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:

  1. The hook opens a Cloud approval request
  2. The approver gets a Slack/Telegram/console notification
  3. They review and approve/deny
  4. 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 are owner-only and kept outside the project. 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 the state directory (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

Metadata

Release files for tenuo-claude-code 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tenuo-claude-code 0.7.0
File Size Uploaded Provenance Actions
tenuo_claude_code-0.7.0.tar.gz 667.9 kB Without provenance Details for tenuo_claude_code-0.7.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for tenuo-claude-code 0.7.0
File Interpreter ABI Platform Actions
tenuo_claude_code-0.7.0-py3-none-any.whl Python 3 none any Details for tenuo_claude_code-0.7.0-py3-none-any.whl

Total release size: 799.6 kB

Release files / tenuo_claude_code-0.7.0.tar.gz

Download URL tenuo_claude_code-0.7.0.tar.gz
Size 667.9 kB
Tags Source
SHA-256 checksum
How to use checksums
3874f09c7159c6cdeb97c27142eb3e723d486804e0c32280cc80a3f69126c1e1
BLAKE2b-256 checksum
How to use checksums
a37716d84b45c5cd56332f2ac2ad87567605c8cda0c2dccc11703234a8d31533
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","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.7.0-py3-none-any.whl

Download URL tenuo_claude_code-0.7.0-py3-none-any.whl
Size 131.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1a619eeb26cdcfc64498eb7714ad8e198483d6f87f86f69ee5edb5bf6ff06e3c
BLAKE2b-256 checksum
How to use checksums
a1760bb48d0af92de1c7fbe04b776f70d4f774b65279d8520b7cd5f0b5a36c1a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","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}

Security for tenuo-claude-code 0.7.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page