Metadata-Version: 2.4
Name: tenuo-claude-code
Version: 0.6.1
Summary: Tenuo governance for Claude Code — warrants, hooks, MCP proxy, and Cloud lifecycle
Project-URL: Homepage, https://tenuo.ai
Project-URL: Repository, https://github.com/tenuo-ai/claude-governance
Project-URL: Documentation, https://github.com/tenuo-ai/claude-governance#readme
Project-URL: Issues, https://github.com/tenuo-ai/claude-governance/issues
Author: Tenuo Contributors
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agents,claude-code,governance,mcp,security,warrants
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: certifi>=2024.0
Requires-Dist: cryptography>=42.0
Requires-Dist: mcp>=1.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: tenuo==0.2.3
Description-Content-Type: text/markdown

# Tenuo for Claude Code

[![PyPI](https://img.shields.io/pypi/v/tenuo-claude-code)](https://pypi.org/project/tenuo-claude-code/)
[![Python](https://img.shields.io/pypi/pyversions/tenuo-claude-code)](https://pypi.org/project/tenuo-claude-code/)
[![CI](https://github.com/tenuo-ai/claude-governance/actions/workflows/ci.yml/badge.svg)](https://github.com/tenuo-ai/claude-governance/actions/workflows/ci.yml)
[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)

Claude Code agents can read files, run shell commands, fetch URLs, call MCP tools,
and spawn subagents. [Tenuo](https://github.com/tenuo-ai/tenuo) adds cryptographic
access control at that action boundary: every model-invoked file read, shell
command, web fetch, MCP call, and subagent action is authorized before it runs.

You write the policy in `tenuo.yaml`; Tenuo compiles it into a signed, expiring
credential called a warrant. A local authorizer checks each tool call against
that warrant and logs the decision. The same boundary applies no matter why the
model tried the call: prompt injection, hallucination, poisoned tool output, or
an unsafe request.

Start with the local quickstart below. Optional Cloud control-plane setup is in
[Cloud mode](#cloud-mode).

## Quickstart

Requires Python 3.10+. The most predictable first run uses the native authorizer
binary; Docker is optional. On Windows, run these commands from WSL.

```bash
pip install tenuo-claude-code
tenuo-claude install-authorizer
mkdir my-project && cd my-project
TENUO_AUTHORIZER_BACKEND=native tenuo-claude bootstrap --yes
```

`bootstrap` writes a starter `tenuo.yaml`, starts the authorizer, and runs a self-test (`verify`). It runs non-interactively, ideal for a fresh folder. For a guided wizard with prompts, run `tenuo-claude onboard` instead; it's the same flow with a preflight `check`. The starter policy is deliberately strict:

```yaml
name: my-project
sandbox: ./workspace
mode: enforce
enforce:
  Read: "subpath:{sandbox}"          # Read only files under ./workspace
  Bash: "shlex:ls,pwd,echo,date"     # Bash only these commands
default: deny                        # every other tool call is denied
```

Now open Claude Code in `my-project/`:

```bash
claude
```

The agent is governed: `Read ./workspace/notes.txt` is allowed; `Read /etc/passwd`, `Bash(curl …)`, or any tool not listed is denied and logged. After Claude runs a few tools, run `tenuo-claude audit --verify` to see and cryptographically verify the local decision receipts. Edit `tenuo.yaml` to fit your project (see [Policy](#policy)), then `tenuo-claude refresh`.

For an example with MCP and subagents, see the [reference demo](demo/).

| Next | Go to |
|------|-------|
| Write the policy | [Policy](#policy) |
| Day-to-day commands | [Commands](#commands) |
| Org root, receipts, approvals, revocation | [Cloud mode](#cloud-mode) |
| Security model and limits | [Security](#security) |
| Something broke | [Troubleshooting](docs/TROUBLESHOOTING.md) · deep dive [docs/DETAILS.md](docs/DETAILS.md) |

## Policy

`tenuo.yaml` is the whole configuration: it drives the warrant, the authorizer, the hooks, and the MCP proxy. You list tools under `enforce:` and give each a constraint on its key argument.

```yaml
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, don't block

enforce:
  Read:  "subpath:{sandbox}"
  Write: "subpath:{sandbox}"
  Bash:  "shlex:ls,pwd,echo,cat,grep"
  WebFetch:
    domains: ["api.github.com", "*.githubusercontent.com"]

default: deny               # anything not listed above is denied

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}"          # bare string constrains the `path` arg
    run_query:                              # constrain a differently-named arg
      arg: sql
      constraint: "regex:^SELECT "
    http_call:                              # constrain several args at once
      args:
        url:    "urlpattern:https://api.example.com/*"
        method: "oneof:GET,HEAD"
```

### Constraints

| Constraint | Applies to | What it checks |
|------------|------------|----------------|
| `subpath:DIR` | path tools (Read, Write, Edit, Glob, Grep) | the path argument must resolve to a location **inside `DIR`**. Symlinks are resolved first, so a link planted in the directory can't point outside it. |
| `shlex:a,b,c` | Bash, Monitor | the command's **executable** must be one of `a,b,c`, and the command must be a single simple command: pipes, `&&`/`;` chaining, subshells, and shell expansion are rejected. (This allowlists the *verb*, not file paths: `cat /etc/passwd` passes if `cat` is allowed. Use `Read`/`Write` to scope files; drop `Bash` for a hard lock.) |
| `domains` / `cidrs` / `schemes` / `ports` | WebFetch | the URL's host must match an allowed domain (`*` matches one label) or CIDR range, **and** pass SSRF hygiene: https-only by default (override with `schemes`), optional `ports` allowlist, with loopback, cloud-metadata IPs, encoded-IP tricks, and spoofed hosts (`api.github.com.evil.com`) blocked. |
| `oneof:a,b` · `notoneof:a,b` · `exact:v` · `pattern:glob` · `regex:re` · `range:min,max` · `urlpattern:url` · `cidr:n/m` | any tool argument | the value must be in the set / not in the set / equal / glob-match / regex-match / fall in the numeric range (either bound may be blank) / match the URL glob / fall inside the IP range (for any IP-based tool, not just WebFetch). |

The keys above are the `tenuo.yaml` DSL's convenient subset. The underlying tenuo engine supports more (set operations, numeric ranges, negation, boolean composition, and CEL expressions) for programmatic policies; see [tenuo](https://github.com/tenuo-ai/tenuo).

`{sandbox}` is a convenience variable for the directory in `sandbox:`; you can point `subpath:` at any path. Tools you don't list aren't governed individually; they're caught by `default`.

For internal egress, `WebFetch` also accepts `cidrs:` to allow hosts by IP range (e.g. `cidrs: ["10.0.0.0/8"]` alongside `domains:`). It's off by default and reaches past public domains and SSRF private-network defaults, so add it deliberately.

**Command-execution tools.** `Bash`, `PowerShell`, and `Monitor` all execute commands and are each governed independently (their own constraint on the `command` argument). `Monitor` runs the same shell commands as `Bash` in the background; `PowerShell` is a different dialect, so prefer `oneof`/`pattern`/`regex` over `shlex` (which parses POSIX syntax) for it. If your team enables `PowerShell` or `Monitor` in Claude Code, list them under `enforce:` too. Left unlisted they fall to `default` (`deny`, or `approve` for a Cloud approval gate). Never put a shell on the `allow:` permit-list either: that grants it **unconstrained**. Always govern shells with an explicit `enforce:` constraint.

**MCP tool arguments.** Under `mcp.enforce:`, a bare constraint string targets the `path` argument. To constrain a differently-named argument use `arg: NAME` + `constraint:`, and to constrain several at once use `args: {NAME: constraint, …}`. This works for any downstream MCP tool and any constraint kind, locally and on Cloud. Tools you don't list are still allowed/denied by `default` and can be human-approval gated (`approval:`); they just aren't argument-constrained.

**Three lists, plus two switches.** Put tools under `enforce:` (constrained), `allow:` (permitted but **unconstrained**: allowed and logged), and let everything else fall to `default:`. `mode:` is the global posture for **every** tool; `default:` is only the catch-all for tools in neither list.

- **`mode: enforce`** blocks denied calls. **`mode: dry-run`** computes and logs the same decisions but blocks nothing, even for tools listed under `enforce:`; use it to shadow a policy, then switch to `enforce`. (`mode: audit` is a deprecated alias for `dry-run`.)
- **`default: deny`** denies any unlisted tool (recommended, fail-closed). **`default: approve`** routes unlisted tools to a human-approval gate instead (Tenuo Cloud only; locally it falls back to deny). There is no permissive catch-all: `default: allow` / `default: audit` are no longer supported (enforce must not fail open) and collapse to `deny`. To permit specific tools without a constraint, list them under `allow:`; to observe everything without blocking, use `mode: dry-run`.
- In `mode: dry-run` nothing is enforced, so `default:` has no effect until you switch back to `enforce`.
- **`subagents:`** declares roles; spawning is gated to those roles, and each runs under the session warrant **attenuated** to its `tools` (it can only ever do less than the session). [Details](docs/DETAILS.md#subagents).
- **`ttl_seconds:`** sets the session warrant lifetime in seconds (positive integer; default `3600` = 1h). `up` re-mints before expiry. Enterprise rollouts pin it centrally through Claude Code managed settings / MDM (the policy file is what managed settings pins); local dev sets it directly. A shorter TTL shrinks the blast radius of a leaked warrant.

Ready-made policies: [examples/policies/](examples/policies/). After any edit, run `tenuo-claude refresh`.

## Commands

Day to day, you mostly need `up` (start), `audit` (review), and `refresh` (after editing policy).

| Command | What it does |
|---------|--------------|
| `onboard` | Interactive first-run wizard (`--local` / `--cloud`); same flow as `bootstrap` but prompts and runs a preflight `check`. Scaffolds an example policy if you don't have one. |
| `bootstrap` | First-run quickstart (used above): non-interactive scaffold starter policy (if none) → `init` → `up` → `verify`. `--cloud` for Cloud; set `TENUO_AUTHORIZER_BACKEND=native` to force the installed native authorizer. |
| `init` | Compile an **existing** `tenuo.yaml`: mint the warrant, wire the PreToolUse hook and MCP proxy. Pass `--scaffold` to write an example if none exists (it no longer does so automatically). |
| `up` / `down` | Start / stop the authorizer (auto-selects Docker or native; `--native` to force). |
| `refresh` | Recompile after editing `tenuo.yaml` (restarts the authorizer if running). In Cloud mode, warns if capability rules drifted from the last `tenuo-admin setup`. |
| `verify [--deep]` | Self-test the live policy against the authorizer (no Claude session needed). `--deep` adds an SSRF / encoded-IP matrix and extra Bash deny cases, a reproducible artifact for security review; when the local Claude CLI can load project settings, it also runs a live PreToolUse exit-code harness. |
| `audit [--tail N] [--verify]` | Show the decision log (`.state/receipts.jsonl`). `--verify` checks receipt signatures, hash-chain links, and embedded authorization evidence. |
| `check` | Preflight: dependencies, wiring, audit-sink health, leaked admin keys, and (Cloud) control-plane bindings. |
| `status` | Warrant, mode, audit-sink health, and Cloud summary. |
| `install-authorizer` | Install the pinned native authorizer to `~/.tenuo/bin` from the Tenuo release assets; Cargo is only a fallback if no prebuilt asset exists. |
| `bench [--json]` | Measure per-call overhead on your machine (PoP sign, authorizer round-trip, full hook path). |
| `revoke` | Revoke the current session warrant. |

The warrant is short-lived (~1h TTL); `up` refreshes it. In normal local mode, the authorizer listens on `127.0.0.1:9090`; change it with `TENUO_AUTHORIZER_PORT` before `bootstrap`. In managed enterprise mode, the generated system service uses a root-owned Unix socket instead. Generated files (don't commit): `.state/` (keys, warrant, credentials), `.claude/settings.json` (hooks), `.mcp.json` (MCP wiring).

Working from a git clone instead of PyPI? See [Build from source](#build-from-source).

## How enforcement works

Tenuo enforces at Claude Code's action boundary. `init` compiles `tenuo.yaml` into a signed warrant and wires two interception points; both check the **same** warrant against the **same** local authorizer:

- **Native tools** (Read, Bash, WebFetch, …) → a Claude Code **PreToolUse hook** intercepts the call, signs a proof-of-possession with the session key, and asks the authorizer.
- **MCP tools** → Claude is pointed at a **proxy** that stands in for the downstream MCP server; the proxy authorizes, then forwards only if allowed.

![Architecture](https://raw.githubusercontent.com/tenuo-ai/claude-governance/main/docs/images/tenuo_claude_code_architecture.png)

The diagram shows the default local/Docker path. Managed enterprise deployments use the same hook/proxy shape, but the system-pinned authorizer is reached over a root-owned Unix socket (native on macOS, Docker-backed on Linux).

The authorizer (a small local service, ~1–3 ms/call) verifies the warrant's signature, proof-of-possession, and expiry, then checks the call's arguments against the warrant's constraints → allow, deny, or (Cloud) approval-required. Full detail in [docs/DETAILS.md](docs/DETAILS.md).

## Cloud mode

Local mode is enough to evaluate Tenuo on one project. Connect [cloud.tenuo.ai](https://cloud.tenuo.ai) for organization-scale governance:

- **Tenant-root warrants**: sessions chain to your org root, not a key on the laptop.
- **Signed receipts**: one verifiable allow/deny/approval audit stream (Ed25519 over CBOR).
- **Fleet revocation**: revoke a warrant id; authorizers pick it up within ~30s.
- **Human approval gates**: specific calls pause for a person instead of allow/deny ([below](#human-approval-cloud)).
- **Managed rollout**: push hook/MCP wiring through Claude Code managed settings instead of per-project local settings.

![Cloud audit stream: a verifiable list of allow, deny, and approved receipts](https://raw.githubusercontent.com/tenuo-ai/claude-governance/main/docs/images/cloud-audit-stream.png)

### Setup

Two keys, kept apart; the runtime never sees the admin key:

| Key | From | Goes in | Used by |
|-----|------|---------|---------|
| **Runtime** (`tenuo_ct_…`) | cloud.tenuo.ai → Agents → Quick Connect → **Authorizer Only** | `.state/cloud.env` | `tenuo-claude up`, hooks |
| **Tenant-admin** (`tc_…`) | Settings → API Keys → Create (admin role) | `~/.tenuo/admin.env` | `tenuo-admin setup` (once) |

```bash
mkdir my-project && cd my-project
# First Cloud setup needs BOTH keys from the table above: the runtime Quick Connect
# token (fires the warrant) AND the tenant-admin key in ~/.tenuo/admin.env (the
# `tenuo-admin setup` step that publishes the trigger).
tenuo-claude install-authorizer
TENUO_AUTHORIZER_BACKEND=native tenuo-claude bootstrap --cloud --yes \
  --connect-token "$TENUO_CONNECT_TOKEN" \
  --admin-key "$TENUO_ADMIN_KEY"
```

> First-time Cloud setup requires **both** credentials: the runtime token and the tenant-admin key. Every session after that needs only the runtime token.

Cloud includes a **Claude Code Governance Starter** warrant template for the recommended shape. You do not create a trigger from it by hand: `tenuo-admin setup` compiles this project's `tenuo.yaml`, MCP rules, holder, and approval policy into the Cloud trigger.

Every session after that:

```bash
tenuo-claude check && tenuo-claude up
```

If `check` reports a **cloud bindings** failure, run `tenuo-admin setup` and retry.

> `tenuo-claude up` refuses to start if a tenant-admin key is in the environment; keep it only in `~/.tenuo/admin.env`. And once a project is on Cloud, don't re-run plain `bootstrap`: it reverts the project to local mode and moves your Cloud files aside. Use `check && up`.

CI / non-interactive and manual step-by-step setup: [docs/DETAILS.md § Tenuo Cloud](docs/DETAILS.md#tenuo-cloud-extended). After changing `enforce`/`mcp`/`subagents`/approvals on Cloud, re-run `tenuo-admin setup`; for `mode`-only changes, `refresh` suffices.

### Human approval (Cloud)

A gated capability returns a third outcome, `approval-required`, instead of allow/deny. The hook opens a Cloud approval request, waits for an approver on their notification channel (Slack, Telegram, console, …), then re-authorizes with their **signed, identity-bound** approval, so the receipt records *who* approved. Add an `approval:` block to **any enforced native tool** (e.g. `Bash`), **any `mcp.enforce` tool**, or `WebFetch`; an optional `exempt:` lets safe argument values skip the gate. `default: approve` gates every unlisted tool.

**Human approval requires Tenuo Cloud, anywhere (native hook, MCP proxy, or catch-all).** The gate lives in the Cloud-issued warrant, so without Cloud an approval-gated tool falls back to **deny** (fail-closed); `tenuo-claude check` warns when a gate is configured but Cloud isn't. First time setting this up? Follow the step-by-step [approval setup runbook](docs/DETAILS.md#approval-setup-runbook) (the approver identity and its notification channel are created in the Tenuo Cloud console). Policy shape and mechanics: [docs/DETAILS.md § Human approval](docs/DETAILS.md#human-approval-cloud).

For a first approval smoke test, add `--advanced --approver-id <Cloud identity id>` (or `--approver "<display name>"` for demos) to `bootstrap --cloud`. The generated advanced overlay gates off-allowlist `WebFetch` calls for approval while allowing `docs.anthropic.com`; SSRF/metadata URLs remain hard-denied.

For MCP, approval and constraints use the concrete capability names in `mcp.enforce` (for example `mcp__server__tool` on the Claude hook path). Unlisted MCP tools follow `default`.

![Receipt drill-down: a human approval with the approver identity and request hash](https://raw.githubusercontent.com/tenuo-ai/claude-governance/main/docs/images/cloud-receipt-approval-detail.png)

## Security

Tenuo runs **alongside** Claude Code permissions; it doesn't replace managed settings. The difference is where and how policy is enforced:

| | Claude Code permissions | Tenuo warrant |
|---|---|---|
| Form | Allow/ask/deny rules in settings | Signed, expiring capability token; Cloud chains to your org root |
| Enforcement point | Claude's permission UI | PreToolUse hook + MCP proxy, checked by the authorizer |
| `--dangerously-skip-permissions` | Skips the prompts | Does not disable installed Tenuo hook/proxy checks |
| Expiry | Until edited | Session TTL (1h default, set via `ttl_seconds:`); `up` refreshes |
| Revocation | Edit rules (live sessions may keep allowances) | Revoke warrant id → ~30s fleet sync (Cloud) |
| Evidence | Optional hook logs | Local JSONL; signed receipt stream in Cloud |
| Org deployment | Per-user settings, locally editable | Managed-settings hooks + shared policy |

Admins can also block the bypass flag entirely in managed settings (`disableBypassPermissionsMode`).

**What's in scope.** Tenuo governs **model-invoked tool calls** (Read, Bash, WebFetch, MCP tools, subagent spawns) on the PreToolUse path, including the agent's own Bash. The TUI `!` shell (a command the *operator* types) is not a tool call and is out of scope; the model can't invoke it. ([details](docs/DETAILS.md#agent-tools-vs-operator-shell))

**Fail-closed.** A missing or broken `tenuo.yaml` denies every governed call until it's restored. Keys under `.state/` must be owner-only (`0600`).

**Receipts.** Every governed call carries a proof-of-possession signature the authorizer verifies. Locally, the hook appends a signed JSON line to `.state/receipts.jsonl` (read with `tenuo-claude audit --verify`; in `mode: dry-run`, denials show as `WOULD-DENY`):

```json
{"phase":"pre","decision":"deny","claude_tool":"Read","governed":true,
 "args":{"file_path":"/etc/passwd"},"reason":"Constraint not satisfied"}
```

Local `audit --verify` checks receipt signatures, hash-chain links, warrant chains, and deterministic constraint replay. Approval signatures are recorded when used, but independent approver/threshold verification belongs to Cloud audit because it depends on the Cloud approval policy. Connected to Cloud, the authorizer also emits signed receipts to your tenant, the central record for compliance and fleet audit.

**Rolling out to a team.** Keep `tenuo.yaml` in version control, push the hook/MCP wiring through Claude Code **managed settings** (not per-developer `settings.local.json`), and use Cloud for org-root warrants, central audit, and revocation. Generate the pinned artifacts with `tenuo-claude managed-template` (see [examples/managed](examples/managed/) for the rollout checklist, Unix-socket smoke test, and hardened `--socket-group` option). Start in `mode: dry-run`, review the `WOULD-DENY` rows, then switch to `enforce`. [Talk to us](https://tenuo.ai/early-access.html) about managed-settings rollout. Report issues: [SECURITY.md](SECURITY.md).

**Turning it off.** `tenuo-claude disable` removes Tenuo's hook wiring (so Claude Code / Cursor stop calling the authorizer) and stops it, leaving your policy and warrant in place; re-enable with `tenuo-claude up`. `tenuo-claude uninstall` goes further and also deletes `.state/` (warrant, keys, gateway, receipts, Cloud credentials); `tenuo.yaml` is never touched. Use `--keep-state` to unwire without deleting state, or `--yes` to skip the prompt.

## Build from source

For development, running the demo from a checkout, or using `./bin/tenuo-claude`:

```bash
git clone https://github.com/tenuo-ai/claude-governance.git
cd claude-governance
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 you don't use 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. The hooks pin the launcher path at wiring time. Contributors: [CONTRIBUTING.md](CONTRIBUTING.md).

## Performance

Authorization is ~1–3 ms per call; the command hook adds ~100–200 ms (mostly process startup). Measure on your machine with `tenuo-claude bench` after `up`.

## This repo

GitHub: [`tenuo-ai/claude-governance`](https://github.com/tenuo-ai/claude-governance) · PyPI: [`tenuo-claude-code`](https://pypi.org/project/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/` | [Implementation details](docs/DETAILS.md) · [Troubleshooting](docs/TROUBLESHOOTING.md) |
