tenuo-claude-code 0.6.4__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. tenuo_claude_code-0.7.0/.claude-plugin/marketplace.json +26 -0
  2. tenuo_claude_code-0.7.0/.gitattributes +2 -0
  3. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/CONTRIBUTING.md +12 -3
  4. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/PKG-INFO +143 -61
  5. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/README.md +134 -54
  6. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/README.md +1 -1
  7. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/DETAILS.md +5 -5
  8. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/ENTERPRISE.md +1 -1
  9. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/TROUBLESHOOTING.md +7 -7
  10. tenuo_claude_code-0.7.0/docs/releases/0.7.0.md +37 -0
  11. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/examples/managed/README.md +10 -6
  12. tenuo_claude_code-0.7.0/plugins/tenuo/.claude-plugin/plugin.json +15 -0
  13. tenuo_claude_code-0.7.0/plugins/tenuo/hooks/hooks.json +41 -0
  14. tenuo_claude_code-0.7.0/plugins/tenuo/scripts/tenuo-hook +88 -0
  15. tenuo_claude_code-0.7.0/plugins/tenuo/scripts/tenuo_plugin.py +447 -0
  16. tenuo_claude_code-0.7.0/plugins/tenuo/skills/audit/SKILL.md +16 -0
  17. tenuo_claude_code-0.7.0/plugins/tenuo/skills/status/SKILL.md +21 -0
  18. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/pyproject.toml +14 -7
  19. tenuo_claude_code-0.7.0/requirements.txt +3 -0
  20. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/__init__.py +1 -1
  21. tenuo_claude_code-0.7.0/src/tenuo_claude_code/action_identity.py +61 -0
  22. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/admin.py +6 -244
  23. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/authorizer_runtime.py +33 -3
  24. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/cli.py +594 -718
  25. tenuo_claude_code-0.7.0/src/tenuo_claude_code/cloud_policy.py +253 -0
  26. tenuo_claude_code-0.7.0/src/tenuo_claude_code/constraints.py +141 -0
  27. tenuo_claude_code-0.7.0/src/tenuo_claude_code/managed_runtime.py +63 -0
  28. tenuo_claude_code-0.7.0/src/tenuo_claude_code/mcp_proxy.py +111 -0
  29. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs/filesystem-dev/pack.yaml +11 -5
  30. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs/filesystem-dev/tenuo.yaml.tmpl +6 -2
  31. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs/github-mcp/pack.yaml +14 -10
  32. tenuo_claude_code-0.7.0/src/tenuo_claude_code/packs/github-mcp/tenuo.yaml.tmpl +117 -0
  33. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/paths.py +43 -1
  34. tenuo_claude_code-0.7.0/src/tenuo_claude_code/policy.py +375 -0
  35. tenuo_claude_code-0.7.0/src/tenuo_claude_code/routing.py +107 -0
  36. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/conftest.py +9 -0
  37. tenuo_claude_code-0.7.0/tests/test_action_identity.py +127 -0
  38. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_admin.py +15 -9
  39. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_authorizer_runtime.py +50 -2
  40. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_check.py +1 -0
  41. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_dx_improvements.py +21 -0
  42. tenuo_claude_code-0.7.0/tests/test_grep_exclusions.py +123 -0
  43. tenuo_claude_code-0.7.0/tests/test_managed_runtime.py +76 -0
  44. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_managed_template.py +3 -2
  45. tenuo_claude_code-0.7.0/tests/test_mcp_proxy.py +163 -0
  46. tenuo_claude_code-0.7.0/tests/test_packs.py +275 -0
  47. tenuo_claude_code-0.7.0/tests/test_plugin_launcher.py +520 -0
  48. tenuo_claude_code-0.7.0/tests/test_policy_validation.py +62 -0
  49. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_posture.py +19 -0
  50. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_routing.py +45 -20
  51. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_scaffold.py +35 -1
  52. tenuo_claude_code-0.7.0/tests/test_state_dir.py +178 -0
  53. tenuo_claude_code-0.7.0/tests/test_tenuo_files.py +66 -0
  54. tenuo_claude_code-0.6.4/src/tenuo_claude_code/packs/github-mcp/tenuo.yaml.tmpl +0 -86
  55. tenuo_claude_code-0.6.4/tests/test_mcp_proxy.py +0 -53
  56. tenuo_claude_code-0.6.4/tests/test_packs.py +0 -160
  57. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/.gitignore +0 -0
  58. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/LICENSE +0 -0
  59. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/.claude/agents/researcher.md +0 -0
  60. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/.mcp.json +0 -0
  61. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/docs/README.md +0 -0
  62. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/fake-secrets.env +0 -0
  63. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/ops_server.py +0 -0
  64. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/sandbox/checkout-evidence.log +0 -0
  65. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/sandbox/incident-report.md +0 -0
  66. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/sandbox/notes.txt +0 -0
  67. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/tenuo.yaml +0 -0
  68. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/tenuo_demo.py +0 -0
  69. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/images/cloud-audit-stream.png +0 -0
  70. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/images/cloud-receipt-approval-detail.png +0 -0
  71. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/images/tenuo_claude_code_architecture.png +0 -0
  72. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/images/tenuo_claude_code_architecture.svg +0 -0
  73. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/examples/policies/README.md +0 -0
  74. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/examples/policies/dry-run-rollout.yaml +0 -0
  75. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/examples/policies/enforce-with-mcp.yaml +0 -0
  76. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/examples/policies/read-only-research.yaml +0 -0
  77. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/data/harness_tools.yaml +0 -0
  78. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs/http-api-safe/pack.yaml +0 -0
  79. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs/http-api-safe/tenuo.yaml.tmpl +0 -0
  80. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs.py +0 -0
  81. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/templates/tenuo.yaml.example +0 -0
  82. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/verify.py +0 -0
  83. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/README.md +0 -0
  84. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/admin.env.example +0 -0
  85. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/cloud.env.example +0 -0
  86. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/tenuo.yaml.advanced.example +0 -0
  87. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/tenuo.yaml.cloud.example +0 -0
  88. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/tenuo.yaml.example +0 -0
  89. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_approvals.py +0 -0
  90. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_authorize_call.py +0 -0
  91. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_cloud_bindings.py +0 -0
  92. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_constraints.py +0 -0
  93. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_ipc_transport.py +0 -0
  94. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_permissions.py +0 -0
  95. {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_subagents.py +0 -0
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "tenuo",
3
+ "owner": {
4
+ "name": "Tenuo",
5
+ "email": "daniel@tenuo.ai",
6
+ "url": "https://tenuo.ai"
7
+ },
8
+ "description": "Tenuo plugins for Claude Code.",
9
+ "plugins": [
10
+ {
11
+ "name": "tenuo",
12
+ "displayName": "Tenuo",
13
+ "source": "./plugins/tenuo",
14
+ "description": "Task-scoped authorization for Claude Code: warrants constrain tools and arguments before each call, with signed receipts. Inactive until a project opts in with tenuo.yaml.",
15
+ "author": {
16
+ "name": "Tenuo",
17
+ "url": "https://tenuo.ai"
18
+ },
19
+ "homepage": "https://tenuo.ai",
20
+ "repository": "https://github.com/tenuo-ai/tenuo-claude-code",
21
+ "license": "Apache-2.0",
22
+ "category": "security",
23
+ "tags": ["security", "authorization", "governance", "audit"]
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,2 @@
1
+ # This shell entry point must also run from Windows checkouts copied into WSL.
2
+ /plugins/tenuo/scripts/tenuo-hook text eol=lf
@@ -20,7 +20,7 @@ Re-run `init` or `refresh` after switching Python venvs (hooks pin `sys.executab
20
20
  2. Keep changes focused; match existing style in `src/tenuo_claude_code/`.
21
21
  3. Run `uv run --with pytest pytest` (fast unit tests, no Docker). If you touch enforcement
22
22
  or policy wiring, also run `cd demo && tenuo-claude verify` (live authorizer).
23
- 4. Do not commit `.state/`, real API keys, or home-directory paths.
23
+ 4. Do not commit the state directory, real API keys, or home-directory paths.
24
24
 
25
25
  Security issues: see [SECURITY.md](SECURITY.md). No public issues for vulnerabilities.
26
26
 
@@ -28,7 +28,7 @@ Security issues: see [SECURITY.md](SECURITY.md). No public issues for vulnerabil
28
28
 
29
29
  | Path | Contains |
30
30
  |------|----------|
31
- | `.state/` | Holder/issuer keys, warrant, receipts, `cloud.env` |
31
+ | state directory (`tenuo-claude state-dir`) | Holder/issuer keys, warrant, receipts, `cloud.env` |
32
32
  | `.claude/settings.json` | Generated hook wiring |
33
33
  | `~/.tenuo/admin.env` | Admin key for `tenuo-admin setup` only |
34
34
 
@@ -42,10 +42,16 @@ under `/Users/` or `C:\`.
42
42
 
43
43
  | Module | Role |
44
44
  |--------|------|
45
- | `src/tenuo_claude_code/cli.py` | Policy, hooks, MCP proxy, lifecycle commands |
45
+ | `src/tenuo_claude_code/cli.py` | Project state, hooks, and lifecycle orchestration |
46
+ | `src/tenuo_claude_code/policy.py`, `constraints.py`, `routing.py`, `action_identity.py` | Policy parsing, constraints, routing, and complete request identity without CLI state |
47
+ | `src/tenuo_claude_code/cloud_policy.py` | Cloud warrant configuration compiler |
48
+ | `src/tenuo_claude_code/mcp_proxy.py` | MCP transport adapter with explicit runtime callbacks |
49
+ | `src/tenuo_claude_code/managed_runtime.py` | Administrator-owned Python runtime validation |
46
50
  | `src/tenuo_claude_code/admin.py` | One-time Cloud setup (`tenuo-admin`) |
47
51
  | `src/tenuo_claude_code/verify.py` | Policy-driven authorizer self-test |
48
52
  | `src/tenuo_claude_code/data/harness_tools.yaml` | Bundled audit-allow tool list |
53
+ | `plugins/tenuo/` | Claude Code plugin (hooks, launcher, `/tenuo:*` skills); decision table tested in `tests/test_plugin_launcher.py` |
54
+ | `.claude-plugin/marketplace.json` | Marketplace entry so `/plugin marketplace add` works on this repo |
49
55
 
50
56
  Root `scripts/tenuo_claude.py` / `scripts/tenuo_admin.py` are thin shims; prefer `tenuo-claude` on PATH.
51
57
 
@@ -61,6 +67,9 @@ or `PYPI_TOKEN` secret).
61
67
  Release steps:
62
68
 
63
69
  1. Bump `version` in `pyproject.toml`, `src/tenuo_claude_code/__init__.py`, and `uv.lock`.
70
+ If anything under `plugins/tenuo/` changed, also bump `version` in
71
+ `plugins/tenuo/.claude-plugin/plugin.json` (installed plugins only update when it changes)
72
+ and run `claude plugin validate . --strict` and `claude plugin validate plugins/tenuo --strict`.
64
73
  2. Commit (`Release X.Y.Z`) and push the `vX.Y.Z` tag (this triggers the PyPI publish).
65
74
  3. Always publish brief release notes:
66
75
  `gh release create vX.Y.Z --verify-tag --title vX.Y.Z --notes "<one short paragraph or a few bullets>"`.
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tenuo-claude-code
3
- Version: 0.6.4
3
+ Version: 0.7.0
4
4
  Summary: Tenuo governance for Claude Code — warrants, hooks, MCP proxy, and Cloud lifecycle
5
5
  Project-URL: Homepage, https://tenuo.ai
6
- Project-URL: Repository, https://github.com/tenuo-ai/claude-governance
7
- Project-URL: Documentation, https://github.com/tenuo-ai/claude-governance#readme
8
- Project-URL: Issues, https://github.com/tenuo-ai/claude-governance/issues
6
+ Project-URL: Repository, https://github.com/tenuo-ai/tenuo-claude-code
7
+ Project-URL: Documentation, https://github.com/tenuo-ai/tenuo-claude-code#readme
8
+ Project-URL: Issues, https://github.com/tenuo-ai/tenuo-claude-code/issues
9
9
  Author: Tenuo Contributors
10
10
  License-Expression: Apache-2.0
11
11
  License-File: LICENSE
@@ -23,67 +23,59 @@ Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Topic :: Security
24
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
25
  Requires-Python: >=3.10
26
- Requires-Dist: certifi>=2024.0
26
+ Requires-Dist: anyio>=4.14.2
27
+ Requires-Dist: certifi>=2026.10.18
27
28
  Requires-Dist: cryptography>=42.0
28
- Requires-Dist: mcp<2,>=1.0
29
+ Requires-Dist: mcp<2,>=1.30
30
+ Requires-Dist: pyjwt>=2.15.0
29
31
  Requires-Dist: pyyaml>=6.0
30
- Requires-Dist: tenuo<0.4,>=0.3.0
32
+ Requires-Dist: tenuo<0.4,>=0.3.6
31
33
  Description-Content-Type: text/markdown
32
34
 
33
35
  # Tenuo for Claude Code
34
36
 
35
37
  [![PyPI](https://img.shields.io/pypi/v/tenuo-claude-code)](https://pypi.org/project/tenuo-claude-code/)
36
38
  [![Python](https://img.shields.io/pypi/pyversions/tenuo-claude-code)](https://pypi.org/project/tenuo-claude-code/)
37
- [![CI](https://github.com/tenuo-ai/tenuo-claude-code/actions/workflows/ci.yml/badge.svg)](https://github.com/tenuo-ai/tenuo-claude-code/actions/workflows/ci.yml)
39
+ [![CI](https://github.com/tenuo-ai/tenuo-claude-code/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/tenuo-ai/tenuo-claude-code/actions/workflows/ci.yml?query=branch%3Amain)
38
40
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
39
41
 
40
- **Task-scoped authorization for Claude Code.**
42
+ **Claude Code's permissions decide which tools Claude can call. Tenuo decides what it can call them with.**
41
43
 
42
- 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.
44
+ 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.
43
45
 
44
- 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.
46
+ Here Claude Code's own permissions allowed the GitHub MCP server's `get_file_contents`. The Tenuo policy allows one repository:
45
47
 
46
- ## Why Tenuo?
47
-
48
- Claude Code permissions prompt the user; Tenuo checks the **model's action** at the authorizer boundary. The difference matters:
49
-
50
- | | Claude Code permissions | Tenuo warrant |
51
- |---|---|---|
52
- | **Trigger** | User prompt when Claude asks for access | Automatic check before every tool call |
53
- | **Scope** | Tool name only | Tool name + argument values |
54
- | **Bypass** | Prompt can be skipped | Cannot be disabled by the user |
55
- | **Evidence** | Optional hook logs | Signed, tamper-proof receipts |
56
- | **Revocation** | Edit rules (active sessions may keep access) | Instant fleet-wide revocation (Cloud) |
57
- | **Enforcement** | Claude Code settings UI | PreToolUse hook + MCP proxy + authorizer |
58
- | **Span** | Single session | Persists across MCP calls and subagents |
59
-
60
- 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.
61
-
62
- ## What It Looks Like
63
-
64
- After setup, this is what you see:
48
+ ```yaml
49
+ mcp:
50
+ server: github # must match the client MCP server name
51
+ enforce:
52
+ get_file_contents:
53
+ args:
54
+ owner: "exact:tenuo-ai"
55
+ repo: "pattern:tenuo"
56
+ default: deny
57
+ ```
65
58
 
66
59
  ```
67
- tenuo-claude audit --verify
60
+ $ tenuo-claude audit
61
+ ALLOW [gov] mcp__github__get_file_contents -> mcp__github__get_file_contents authorized owner=tenuo-ai path=README.md repo=tenuo
62
+ DENY [gov] mcp__github__get_file_contents -> mcp__github__get_file_contents Constraint not satisfied owner=tenuo-ai path=README.md repo=tenuo-cloud
63
+ DENY [gov] mcp__github__get_file_contents -> mcp__github__get_file_contents Constraint not satisfied owner=torvalds path=README repo=linux
64
+ ```
68
65
 
69
- DENY Read /etc/passwd constraint not satisfied
70
- path outside workspace (subpath:/home/user/project)
66
+ 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.
71
67
 
72
- ALLOW Read /home/user/project/main.py constraint satisfied
73
- DENY Bash rm -rf / command not permitted
74
- 'rm' not in allowlist [ls,pwd,cat,grep]
68
+ ## Why Tenuo?
75
69
 
76
- 3 signed receipts verified:
77
- Ed25519 signatures valid
78
- Hash chain unbroken
79
- Constraints replayed and satisfied
80
- ```
70
+ | | Claude Code permissions | Tenuo |
71
+ |---|---|---|
72
+ | **Matches** | Tool name; command and path patterns for built-in tools | Any argument of any tool, including MCP tools |
73
+ | **Decides** | Allow, deny or ask the user | Allow or deny before the call, against a signed, expiring warrant |
74
+ | **Evidence** | Optional hook logs | Signed, hash-chained receipts (`tenuo-claude audit --verify`) |
75
+ | **Subagents** | Same rules as the session | Each declared subagent gets a narrower warrant |
76
+ | **Teams** | Managed settings | Managed settings plus an org-signed root, revocation and approvals (Cloud) |
81
77
 
82
- Each decision is:
83
- - **Signed** with Ed25519 (cryptographic proof)
84
- - **Hash-chained** (tampering is detected)
85
- - **Constraint-replayed** (warrant terms verified at audit time)
86
- - **Cloud-synced** (if configured, sent to tenant for fleet audit)
78
+ 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.
87
79
 
88
80
  ## Quickstart
89
81
 
@@ -95,19 +87,20 @@ tenuo-claude bootstrap --native --pack filesystem-dev
95
87
  claude
96
88
  ```
97
89
 
98
- 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:
90
+ 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:
99
91
 
100
92
  ```bash
101
93
  tenuo-claude audit --verify
102
94
  ```
103
95
 
104
- to see the signed decision log.
96
+ to see the signed decision log, including what would have been denied.
105
97
 
106
98
  ### What the quickstart does
107
99
 
108
100
  1. **`--pack filesystem-dev`** generates a preconfigured policy:
109
- - Read/write files in your project directory
110
- - Run: ls, pwd, cat, grep (safe commands)
101
+ - Read, edit and search files in your project directory (paths resolved, symlinks followed)
102
+ - Never read or change Tenuo's own files: its keys and state, `tenuo.yaml`, `.mcp.json`, `.claude/settings*.json`
103
+ - 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
111
104
  - Deny everything else by default
112
105
  - Start in `mode: dry-run` (log, don't block)
113
106
 
@@ -124,12 +117,29 @@ tenuo-claude refresh
124
117
  ```
125
118
 
126
119
  **Available policy packs:**
127
- - `filesystem-dev` — Read/write project files, safe commands (default)
120
+ - `filesystem-dev` — Read/write project files, minimal shell (default)
128
121
  - `github-mcp` — GitHub MCP server, constrained API queries
129
122
  - `http-api-safe` — WebFetch with SSRF protection and domain allowlists
130
123
 
131
124
  Browse more: [examples/policies/](examples/policies/).
132
125
 
126
+ ### Limit an MCP server
127
+
128
+ Add the server to Claude Code as usual, then scope it. For the GitHub MCP server:
129
+
130
+ ```bash
131
+ tenuo-claude init --pack github-mcp --param owner=myorg --param repos='service-*' --param approver=any-human
132
+ ```
133
+
134
+ 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:
135
+
136
+ ```bash
137
+ tenuo-claude refresh
138
+ tenuo-claude up
139
+ ```
140
+
141
+ 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](#policy).
142
+
133
143
  **For Cloud mode** (org root, revocation, human approval):
134
144
 
135
145
  ```bash
@@ -138,6 +148,37 @@ tenuo-claude bootstrap --native --pack filesystem-dev --cloud
138
148
 
139
149
  Then follow the Cloud setup prompts. [Details: Cloud mode](#cloud-mode).
140
150
 
151
+ ## Install as a Claude Code plugin
152
+
153
+ The repo is also a Claude Code plugin marketplace. Inside Claude Code:
154
+
155
+ ```text
156
+ /plugin marketplace add tenuo-ai/tenuo-claude-code
157
+ /plugin install tenuo@tenuo
158
+ ```
159
+
160
+ 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:
161
+
162
+ - **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.
163
+ - **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.
164
+ - **A one-line status at session start**, plus `/tenuo:status` (`tenuo-claude check`) and `/tenuo:audit` (`tenuo-claude audit --verify`).
165
+
166
+ How the plugin decides, per tool call:
167
+
168
+ | Project state | PreToolUse result |
169
+ |---|---|
170
+ | No `tenuo.yaml` in the session directory or any parent | Nothing (Claude Code permissions apply) |
171
+ | `tenuo.yaml` but no state directory (never initialized) | Nothing; session start says to run `bootstrap` |
172
+ | `<state>/disabled` present (`tenuo-claude disable`) | Nothing; `tenuo-claude up` or `init` re-arms it |
173
+ | Tenuo hooks already wired by `init`/`bootstrap` (project, user, or managed settings) | Nothing; the wired hook enforces, so no double receipts |
174
+ | Initialized, not wired, `tenuo-claude` found | Delegates to `tenuo-claude _hook` |
175
+ | Initialized, not wired, `tenuo-claude` missing, crashing, or timing out | Deny (fail-closed) |
176
+ | `TENUO_PROJECT_DIR` set to a directory without `tenuo.yaml` | Deny (fail-closed) |
177
+
178
+ 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`](plugins/tenuo/scripts/tenuo_plugin.py)); it makes no network calls itself. macOS and Linux only, like the CLI.
179
+
180
+ To try a local checkout: `claude --plugin-dir ./plugins/tenuo`.
181
+
141
182
  ## How It Works
142
183
 
143
184
  You define what Claude can do in `tenuo.yaml`:
@@ -219,7 +260,8 @@ It does **not** govern:
219
260
  ## Fail-Closed Design
220
261
 
221
262
  - Missing or broken `tenuo.yaml` denies every governed call until restored
222
- - Private keys under `.state/` are owner-only (`0600`)
263
+ - Private keys are owner-only (`0600`) and kept outside the project
264
+ - 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
223
265
  - A broken receipt sink surfaces loudly (not silent data loss)
224
266
  - Cloud-disconnected nodes fall back to local policy (no open bypass)
225
267
 
@@ -252,14 +294,51 @@ subagents: # optional: each role runs under a narrower warrant
252
294
  tools: [Read, Grep, Glob]
253
295
 
254
296
  mcp: # optional: govern a downstream MCP server's tools
255
- downstream: ./your_mcp_server.py
297
+ server: tenuo-files # client MCP server name (default: tenuo-files)
298
+ downstream: ./your_mcp_server.py # or {command: npx, args: [...], env: {...}}
256
299
  enforce:
257
300
  read_file: "subpath:{sandbox}"
258
301
  run_query:
259
- arg: sql
260
- constraint: "regex:^SELECT "
302
+ arg: database
303
+ constraint: "oneof:analytics,staging"
304
+ ```
305
+
306
+ 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](docs/releases/0.7.0.md).
307
+
308
+ ### Any MCP client
309
+
310
+ `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`:
311
+
312
+ ```yaml
313
+ mcp:
314
+ server: github
315
+ downstream:
316
+ command: docker
317
+ args: [run, -i, --rm, -e, GITHUB_PERSONAL_ACCESS_TOKEN, ghcr.io/github/github-mcp-server]
318
+ env:
319
+ GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}"
320
+ enforce:
321
+ get_file_contents:
322
+ args: {owner: "exact:myorg", repo: "pattern:service-*"}
323
+ default: deny
324
+ ```
325
+
326
+ Then point the client at the proxy instead of the server, with `tenuo-claude up` running in the project:
327
+
328
+ ```json
329
+ {
330
+ "mcpServers": {
331
+ "github": {
332
+ "command": "tenuo-claude",
333
+ "args": ["mcp-proxy"],
334
+ "env": {"TENUO_PROJECT_DIR": "/path/to/project", "GITHUB_TOKEN": "..."}
335
+ }
336
+ }
337
+ }
261
338
  ```
262
339
 
340
+ A denied call returns an MCP error result (`isError: true`) naming the refused constraint, and is recorded in `tenuo-claude audit`.
341
+
263
342
  ### Constraints
264
343
 
265
344
  | Constraint | Applies to | Checks |
@@ -312,14 +391,17 @@ Day to day: `up` (start), `audit` (review), `refresh` (after editing policy).
312
391
  | `up` / `down` | Start / stop the authorizer (auto-selects Docker or native; `--native` forces native). |
313
392
  | `refresh` | Recompile after editing `tenuo.yaml` (restarts authorizer if running). |
314
393
  | `verify [--deep]` | Self-test the policy against the authorizer (no Claude session needed). `--deep` adds SSRF/encoded-IP matrix and Bash deny cases. |
315
- | `audit [--tail N] [--verify]` | Show the decision log (`.state/receipts.jsonl`). `--verify` checks receipt signatures, hash-chain links, and embedded evidence. |
394
+ | `audit [--tail N] [--verify]` | Show the decision log (`<state>/receipts.jsonl`). `--verify` checks receipt signatures, hash-chain links, and embedded evidence. |
316
395
  | `check` | Preflight: dependencies, wiring, audit-sink health, leaked keys, Cloud bindings. |
317
396
  | `status` | Warrant, mode, audit-sink health, Cloud summary. |
318
397
  | `install-authorizer` | Install the pinned native authorizer to `~/.tenuo/bin`. |
398
+ | `state-dir` | Print this project's state directory. |
319
399
  | `bench [--json]` | Measure per-call overhead on your machine. |
320
400
  | `revoke` | Revoke the current session warrant. |
321
401
 
322
- 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).
402
+ 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).
403
+
404
+ **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.
323
405
 
324
406
  ## Cloud Mode
325
407
 
@@ -330,7 +412,7 @@ For organizations: org root, fleet revocation, human approval, central audit.
330
412
  1. **Create a Tenuo tenant** at [cloud.tenuo.ai](https://cloud.tenuo.ai). Complete the **Infrastructure** onboarding (provisions KMS-backed signing keys).
331
413
 
332
414
  2. **Get credentials:**
333
- - **Runtime key** (`tenuo_ct_…`) from Agents → Quick Connect → **Authorizer Only** → `.state/cloud.env`
415
+ - **Runtime key** (`tenuo_ct_…`) from Agents → Quick Connect → **Authorizer Only** → `<state>/cloud.env`
334
416
  - **Tenant-admin key** (`tc_…`) from Settings → API Keys → Create (admin role) → `~/.tenuo/admin.env`
335
417
 
336
418
  3. **Bootstrap with Cloud:**
@@ -391,7 +473,7 @@ When Claude tries a gated action:
391
473
 
392
474
  **Setup:** See [docs/DETAILS.md § Approval setup runbook](docs/DETAILS.md#approval-setup-runbook).
393
475
 
394
- For Cloud, receipts are sent to your tenant (central audit). Locally, receipts are always written to `.state/receipts.jsonl`.
476
+ For Cloud, receipts are sent to your tenant (central audit). Locally, receipts are always written to `<state>/receipts.jsonl`.
395
477
 
396
478
  ## Security Model
397
479
 
@@ -404,11 +486,11 @@ The warrant is a capability token: it lists what Claude can do and for how long.
404
486
 
405
487
  **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.
406
488
 
407
- **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).
489
+ **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).
408
490
 
409
491
  **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](docs/DETAILS.md#managed-deployment) for the rollout checklist and hardened socket setup.
410
492
 
411
- **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.
493
+ **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.
412
494
 
413
495
  ---
414
496