mirror of
https://github.com/anthropics/claude-code.git
synced 2026-08-13 02:12:26 -06:00
Compare commits
396 Commits
boris/pumg
...
v2.1.219
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c188278cd | |||
| 2982f95155 | |||
| ac062f33ab | |||
| c4dbd740a7 | |||
| 843297f6b1 | |||
| b799fcaf9f | |||
| 4d07874235 | |||
| 015170d3fd | |||
| 07dcb0e135 | |||
| 67f390c9a0 | |||
| c39cb0f14b | |||
| b7784f2c63 | |||
| c9181ca6eb | |||
| 988b3e5643 | |||
| 1fb278b85d | |||
| d4d8fbbb33 | |||
| 15a21e1b4e | |||
| be02c39841 | |||
| d0f5bebd40 | |||
| 00ea292447 | |||
| 7930e1c82d | |||
| c489eb25c7 | |||
| 1322e9bacc | |||
| 125d63feae | |||
| 5dc12eb281 | |||
| 75709eacf1 | |||
| a56ff02e85 | |||
| c80896ca84 | |||
| 3c3558207e | |||
| f605f0b68d | |||
| 27e561ba3d | |||
| 6234fa8f14 | |||
| 01f1617f14 | |||
| f0919a1a72 | |||
| 0bd954331e | |||
| 5c1517a21b | |||
| 2aa6ef3d35 | |||
| 12281998d8 | |||
| b4073894cd | |||
| c487902a53 | |||
| baf38ddaaa | |||
| 4fa369b5b3 | |||
| 423563cfe3 | |||
| 0047022a46 | |||
| 843959fad9 | |||
| 1b7380874c | |||
| 64ceb97caa | |||
| ca9f6045fc | |||
| ee81682a72 | |||
| 5754a8bd4f | |||
| 3a7c736101 | |||
| ca34f27543 | |||
| 1c5f951a48 | |||
| 6a9c2dbe45 | |||
| f967b36c1b | |||
| 72281753c2 | |||
| c1b75cba5e | |||
| 6988846f0f | |||
| feabcc3c2b | |||
| d1e174252d | |||
| b67fa4fa2c | |||
| 625c04c335 | |||
| bdb04fc524 | |||
| 8bae02d531 | |||
| 295dee881d | |||
| 8d0fbf451a | |||
| 2d5c3c6c85 | |||
| 1696f22294 | |||
| 3bb44552af | |||
| b7339920b6 | |||
| d08288e09f | |||
| bf4a74d981 | |||
| ccadef7dcb | |||
| 441892ecee | |||
| 39e853e407 | |||
| 5ef2f06c6a | |||
| 64e53823de | |||
| 2194e8e090 | |||
| 65d44eb134 | |||
| efea4c38d9 | |||
| c6b849f271 | |||
| 15b5d57170 | |||
| 1573399b48 | |||
| cc898dc369 | |||
| b7925a81da | |||
| 69d707009e | |||
| 2962ecd7a9 | |||
| 8bdbb7296d | |||
| d61bfb5b56 | |||
| c5712671c8 | |||
| 6b070c31bc | |||
| fdfbc06c7a | |||
| 831608a360 | |||
| 33a87addb4 | |||
| f7ef09f496 | |||
| 2bd8547920 | |||
| 6cd790cd21 | |||
| fb063cd5e0 | |||
| 60348c9536 | |||
| 52b9f247d1 | |||
| 71135e41b4 | |||
| 5c0e4f96eb | |||
| 9fce4e6ed1 | |||
| 5bf19945e4 | |||
| a243cad119 | |||
| e512ec9918 | |||
| a609cfbee3 | |||
| 1586204194 | |||
| c128568da0 | |||
| 7e936457e4 | |||
| c3933441f0 | |||
| ab3ce06c9a | |||
| a5fa36cac7 | |||
| 925200dffc | |||
| 9afdfd7dc0 | |||
| 2fa67717b8 | |||
| fe53778ed9 | |||
| 0385848b4e | |||
| 71366ecf5d | |||
| 2b53fac3b2 | |||
| bf77ee65bc | |||
| 5a7bf281ba | |||
| 4fb8aa4e0a | |||
| 45ae2f5212 | |||
| f348a16da8 | |||
| 5c18c787f2 | |||
| 194736a4bd | |||
| 550aeecc97 | |||
| 9772e13f82 | |||
| c5600e0b1e | |||
| d2b22528db | |||
| 3c72545dfc | |||
| 54c7be5b3f | |||
| 22fdf68049 | |||
| 227817d0f2 | |||
| b9fbc7796b | |||
| b543a25624 | |||
| 1e03cc7fc4 | |||
| a50a91999b | |||
| b4fa5f85f3 | |||
| 66ab4ae6e0 | |||
| 4411cbae09 | |||
| 2d5c1bab92 | |||
| 78a44f1b7d | |||
| 2923bc87d1 | |||
| f75b6138ef | |||
| a0d9b87038 | |||
| a542f1b4b3 | |||
| cada21c89d | |||
| 6aadfbdca2 | |||
| 16536693ec | |||
| 5e34f198d0 | |||
| a3d9426e3e | |||
| 079dc856c6 | |||
| 420a188467 | |||
| 48b1c6c0ba | |||
| 2dc1e69783 | |||
| db8834ba1d | |||
| 6f049b620f | |||
| 45b5430126 | |||
| f6dbf44cd5 | |||
| 540b61b9fd | |||
| 00553dec20 | |||
| 53a5f3ee07 | |||
| da80366c48 | |||
| 9582ad480f | |||
| 0b3f7cbbbd | |||
| a8335230bc | |||
| 9c63e985f6 | |||
| 38281cfd46 | |||
| 26a1334ef3 | |||
| cd4956871a | |||
| a772bd6091 | |||
| 35b5fe658a | |||
| 1f48d799b9 | |||
| 7ec9125c54 | |||
| 644d6eb37f | |||
| e67079be1f | |||
| 016734047d | |||
| d6ab0eafec | |||
| 76c0cbaeb5 | |||
| 23edca9c9b | |||
| ed58789da7 | |||
| ee4ff289f0 | |||
| b2bab3b743 | |||
| db3858a558 | |||
| a0128f4a40 | |||
| 6e7f65eb95 | |||
| 8799bb0901 | |||
| 05a2bde7be | |||
| 3c917dfe50 | |||
| 8f0fe03e56 | |||
| baf29b882a | |||
| 6aecb15d98 | |||
| 76826f2c80 | |||
| 3592c8be2a | |||
| 3ad3231f0f | |||
| 65dfa9898e | |||
| 0d996a7c34 | |||
| b18f2e7df0 | |||
| b757fc9ecd | |||
| 1718a57495 | |||
| 4523c004dd | |||
| 32c7ff2b6e | |||
| d787369919 | |||
| 8c09097e8c | |||
| edfb5437a4 | |||
| b374a30699 | |||
| a01a88d5ee | |||
| 42c62d73ce | |||
| 1b50583382 | |||
| 232213304d | |||
| a93966285e | |||
| 0931fb76da | |||
| bac22cb316 | |||
| 77df0af778 | |||
| a17040212c | |||
| 76a2154fd5 | |||
| aca4801e91 | |||
| f2a930799b | |||
| 6dcc7d8b76 | |||
| 0a0135f687 | |||
| e74abe58ab | |||
| 7a7bed74e3 | |||
| 9b64827a25 | |||
| 54f0b535b3 | |||
| 675baffdb3 | |||
| bae169824d | |||
| 0b641a77ce | |||
| be5d08fe5f | |||
| 19bb071fe0 | |||
| 85f2807991 | |||
| e7f36bcdf0 | |||
| 2bc62d1456 | |||
| ef1e0ac098 | |||
| d7e3cfb31c | |||
| bd78b216ed | |||
| a4e0c5b4c8 | |||
| 36d9ee2c2e | |||
| 4936302293 | |||
| 43d0eac708 | |||
| f298d940fa | |||
| 34f551fa91 | |||
| 90c07d1c7e | |||
| f93f614768 | |||
| e58014371b | |||
| 5862adf641 | |||
| 38f1f93052 | |||
| cf98f1d943 | |||
| 266d7c8c9f | |||
| 73eddfd640 | |||
| 8c48d2f508 | |||
| 3f9a645986 | |||
| 9f6b6d17de | |||
| e9a9efc121 | |||
| 10e6348e77 | |||
| e431f5b496 | |||
| 052a1317c0 | |||
| a6a8045031 | |||
| 74cc597eb5 | |||
| 923d727492 | |||
| fb3a947cb5 | |||
| 2961ddcafe | |||
| fd8f3801b9 | |||
| 26315247e7 | |||
| 5a91286a82 | |||
| 3196f36cee | |||
| 7d22b6e167 | |||
| 19a829ba68 | |||
| 18979efb8d | |||
| f77acdf149 | |||
| c13cf781ef | |||
| cc70d3ab50 | |||
| 250b257c4e | |||
| dec754edc9 | |||
| 6a2936ab79 | |||
| f860f671dc | |||
| 76df7eea04 | |||
| a8d107f9cc | |||
| b640d94a49 | |||
| b17c088cdc | |||
| a856c62014 | |||
| 0359f24538 | |||
| b8497141a5 | |||
| 9cc635aac1 | |||
| 4f18698a9e | |||
| 553d6ffc3e | |||
| 7b600bca3b | |||
| 4700be03eb | |||
| 9b08c1010b | |||
| f34e2535b4 | |||
| 4297e57ef1 | |||
| 0d0221fd0a | |||
| 5aac2b1b6a | |||
| 2bb8af55fa | |||
| a19dd76dcf | |||
| 63eefe157a | |||
| 870624fc15 | |||
| 2c3884689b | |||
| 03129a27b0 | |||
| a3df424857 | |||
| 0b86fdb0e0 | |||
| c2022d3698 | |||
| e515f50dff | |||
| 24ad98a95f | |||
| 495d6a3d4b | |||
| 5c92b97cc4 | |||
| d213a74fc8 | |||
| 52115592ba | |||
| 5d2df70860 | |||
| b8a2ffb38f | |||
| 9fd556d947 | |||
| b31e2fd182 | |||
| d2cb503247 | |||
| 5fe61207ff | |||
| 1ed82e6af0 | |||
| 2a61cb364c | |||
| 6880bcbace | |||
| 4392352687 | |||
| c27c6f4e4a | |||
| 0dde1fef97 | |||
| e4f682030b | |||
| eb87245010 | |||
| 3680637065 | |||
| 2192c86c20 | |||
| dfd3494132 | |||
| e8cca9a7af | |||
| 6358669884 | |||
| ace0a82778 | |||
| e095e1270a | |||
| 69da5e8269 | |||
| 7069a25987 | |||
| de49a07679 | |||
| cbc55b7d54 | |||
| e836d4ea90 | |||
| 52465789e2 | |||
| 9babb8dbbf | |||
| b1a46e6623 | |||
| 5ef1391afc | |||
| 337cc419c3 | |||
| 5e98326f42 | |||
| 56a3ef77c5 | |||
| 5b69f85043 | |||
| 5e3e9408fe | |||
| 4928f2cdca | |||
| 84d7b08539 | |||
| 10b8736b55 | |||
| 29a5fe7eca | |||
| 26b5c07c59 | |||
| 4ae2cb4e5e | |||
| 3464c7955f | |||
| 5880baedc3 | |||
| 47d996cb4a | |||
| 34dcaa13bc | |||
| a194a9d41b | |||
| eb39543260 | |||
| b83c5cfda3 | |||
| 5a17f570db | |||
| bcda757fff | |||
| 3c95987059 | |||
| 021b91b5eb | |||
| 13a258a3fe | |||
| 68ba47859a | |||
| c508e59e8a | |||
| 8f34f4744d | |||
| 387dc35db7 | |||
| 31ba6e541b | |||
| 59372c0921 | |||
| 68f90e05dd | |||
| 5dd91a9fe2 | |||
| 17945ae3d5 | |||
| d594fd24b9 | |||
| 2b535344fc | |||
| c91a6b660d | |||
| 0b4215d637 | |||
| b85cc4474f | |||
| fc3c2c26e0 | |||
| dd65bc4d16 | |||
| dfd715012f | |||
| 62c3cbc471 | |||
| 556d296786 | |||
| 8a0bfd3687 | |||
| 5d66745e78 | |||
| 18043d7474 | |||
| d38bde5087 | |||
| 970fff49e2 | |||
| 2d0fcacc05 | |||
| f09b24c49a | |||
| 1fe9e369a7 | |||
| b95fa46499 | |||
| 7a05427a4b | |||
| 3af8ef29be | |||
| 84b97165dd | |||
| 07dcea57ee | |||
| 1e95326e12 | |||
| b42fd9928c |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"$schema": "https://json.schemastore.org/claude-code-marketplace.json",
|
||||
"name": "claude-code-plugins",
|
||||
"version": "1.0.0",
|
||||
"description": "Bundled plugins for Claude Code including Agent SDK development tools, PR review toolkit, and commit workflows",
|
||||
@@ -15,14 +15,25 @@
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "pr-review-toolkit",
|
||||
"description": "Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification",
|
||||
"name": "claude-opus-4-5-migration",
|
||||
"description": "Migrate your code and prompts from Sonnet 4.x and Opus 4.1 to Opus 4.5.",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Anthropic",
|
||||
"email": "support@anthropic.com"
|
||||
"name": "William Hu",
|
||||
"email": "whu@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/pr-review-toolkit",
|
||||
"source": "./plugins/claude-opus-4-5-migration",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "code-review",
|
||||
"description": "Automated code review for pull requests using multiple specialized agents with confidence-based scoring to filter false positives",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Boris Cherny",
|
||||
"email": "boris@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/code-review",
|
||||
"category": "productivity"
|
||||
},
|
||||
{
|
||||
@@ -36,6 +47,17 @@
|
||||
"source": "./plugins/commit-commands",
|
||||
"category": "productivity"
|
||||
},
|
||||
{
|
||||
"name": "explanatory-output-style",
|
||||
"description": "Adds educational insights about implementation choices and codebase patterns (mimics the deprecated Explanatory output style)",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Dickson Tsai",
|
||||
"email": "dickson@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/explanatory-output-style",
|
||||
"category": "learning"
|
||||
},
|
||||
{
|
||||
"name": "feature-dev",
|
||||
"description": "Comprehensive feature development workflow with specialized agents for codebase exploration, architecture design, and quality review",
|
||||
@@ -47,6 +69,72 @@
|
||||
"source": "./plugins/feature-dev",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "frontend-design",
|
||||
"description": "Create distinctive, production-grade frontend interfaces with high design quality. Generates creative, polished code that avoids generic AI aesthetics.",
|
||||
"version": "1.1.0",
|
||||
"author": {
|
||||
"name": "Prithvi Rajasekaran & Alexander Bricken",
|
||||
"email": "prithvi@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/frontend-design",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "hookify",
|
||||
"description": "Easily create custom hooks to prevent unwanted behaviors by analyzing conversation patterns or from explicit instructions. Define rules via simple markdown files.",
|
||||
"version": "0.1.0",
|
||||
"author": {
|
||||
"name": "Daisy Hollman",
|
||||
"email": "daisy@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/hookify",
|
||||
"category": "productivity"
|
||||
},
|
||||
{
|
||||
"name": "learning-output-style",
|
||||
"description": "Interactive learning mode that requests meaningful code contributions at decision points (mimics the unshipped Learning output style)",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Boris Cherny",
|
||||
"email": "boris@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/learning-output-style",
|
||||
"category": "learning"
|
||||
},
|
||||
{
|
||||
"name": "plugin-dev",
|
||||
"description": "Comprehensive toolkit for developing Claude Code plugins. Includes 7 expert skills covering hooks, MCP integration, commands, agents, and best practices. AI-assisted plugin creation and validation.",
|
||||
"version": "0.1.0",
|
||||
"author": {
|
||||
"name": "Daisy Hollman",
|
||||
"email": "daisy@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/plugin-dev",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "pr-review-toolkit",
|
||||
"description": "Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Anthropic",
|
||||
"email": "support@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/pr-review-toolkit",
|
||||
"category": "productivity"
|
||||
},
|
||||
{
|
||||
"name": "ralph-wiggum",
|
||||
"description": "Interactive self-referential AI loops for iterative development. Claude works on the same task repeatedly, seeing its previous work, until completion.",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Daisy Hollman",
|
||||
"email": "daisy@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/ralph-wiggum",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "security-guidance",
|
||||
"description": "Security reminder hook that warns about potential security issues when editing files, including command injection, XSS, and unsafe code patterns",
|
||||
@@ -57,39 +145,6 @@
|
||||
},
|
||||
"source": "./plugins/security-guidance",
|
||||
"category": "security"
|
||||
},
|
||||
{
|
||||
"name": "code-review",
|
||||
"description": "Automated code review for pull requests using multiple specialized agents with confidence-based scoring to filter false positives",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Boris Cherny",
|
||||
"email": "boris@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/code-review",
|
||||
"category": "productivity"
|
||||
},
|
||||
{
|
||||
"name": "explanatory-output-style",
|
||||
"description": "Adds educational insights about implementation choices and codebase patterns (mimics the deprecated Explanatory output style)",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Dickson Tsai",
|
||||
"email": "dickson@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/explanatory-output-style",
|
||||
"category": "learning"
|
||||
},
|
||||
{
|
||||
"name": "learning-output-style",
|
||||
"description": "Interactive learning mode that requests meaningful code contributions at decision points (mimics the unshipped Learning output style)",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Boris Cherny",
|
||||
"email": "boris@anthropic.com"
|
||||
},
|
||||
"source": "./plugins/learning-output-style",
|
||||
"category": "learning"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+11
-22
@@ -1,5 +1,5 @@
|
||||
---
|
||||
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh api:*), Bash(gh issue comment:*)
|
||||
allowed-tools: Bash(./scripts/gh.sh:*), Bash(./scripts/comment-on-duplicates.sh:*)
|
||||
description: Find duplicate GitHub issues
|
||||
---
|
||||
|
||||
@@ -11,28 +11,17 @@ To do this, follow these steps precisely:
|
||||
2. Use an agent to view a Github issue, and ask the agent to return a summary of the issue
|
||||
3. Then, launch 5 parallel agents to search Github for duplicates of this issue, using diverse keywords and search approaches, using the summary from #1
|
||||
4. Next, feed the results from #1 and #2 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
|
||||
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates)
|
||||
5. Finally, use the comment script to post duplicates:
|
||||
```
|
||||
./scripts/comment-on-duplicates.sh --potential-duplicates <dup1> <dup2> <dup3>
|
||||
```
|
||||
|
||||
Notes (be sure to tell this to your agents, too):
|
||||
|
||||
- Use `gh` to interact with Github, rather than web fetch
|
||||
- Do not use other tools, beyond `gh` (eg. don't use other MCP servers, file edit, etc.)
|
||||
- Use `./scripts/gh.sh` to interact with Github, rather than web fetch or raw `gh`. Examples:
|
||||
- `./scripts/gh.sh issue view 123` — view an issue
|
||||
- `./scripts/gh.sh issue view 123 --comments` — view with comments
|
||||
- `./scripts/gh.sh issue list --state open --limit 20` — list issues
|
||||
- `./scripts/gh.sh search issues "query" --limit 10` — search for issues
|
||||
- Do not use other tools, beyond `./scripts/gh.sh` and the comment script (eg. don't use other MCP servers, file edit, etc.)
|
||||
- Make a todo list first
|
||||
- For your comment, follow the following format precisely (assuming for this example that you found 3 suspected duplicates):
|
||||
|
||||
---
|
||||
|
||||
Found 3 possible duplicate issues:
|
||||
|
||||
1. <link to issue>
|
||||
2. <link to issue>
|
||||
3. <link to issue>
|
||||
|
||||
This issue will be automatically closed as a duplicate in 3 days.
|
||||
|
||||
- If your issue is a duplicate, please close it and 👍 the existing issue instead
|
||||
- To prevent auto-closure, add a comment or 👎 this comment
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
---
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
allowed-tools: Bash(gh issue list:*), Bash(gh issue view:*), Bash(gh issue edit:*), TodoWrite
|
||||
description: Triage GitHub issues and label critical ones for oncall
|
||||
---
|
||||
|
||||
You're an oncall triage assistant for GitHub issues. Your task is to identify critical issues that require immediate oncall attention and apply the "oncall" label.
|
||||
|
||||
Repository: anthropics/claude-code
|
||||
|
||||
Task overview:
|
||||
|
||||
1. First, get all open bugs updated in the last 3 days with at least 50 engagements:
|
||||
```bash
|
||||
gh issue list --repo anthropics/claude-code --state open --label bug --limit 1000 --json number,title,updatedAt,comments,reactions | jq -r '.[] | select((.updatedAt >= (now - 259200 | strftime("%Y-%m-%dT%H:%M:%SZ"))) and ((.comments | length) + ([.reactions[].content] | length) >= 50)) | "\(.number)"'
|
||||
```
|
||||
|
||||
2. Save the list of issue numbers and create a TODO list with ALL of them. This ensures you process every single one.
|
||||
|
||||
3. For each issue in your TODO list:
|
||||
- Use `gh issue view <number> --repo anthropics/claude-code --json title,body,labels,comments` to get full details
|
||||
- Read and understand the full issue content and comments to determine actual user impact
|
||||
- Evaluate: Is this truly blocking users from using Claude Code?
|
||||
- Consider: "crash", "stuck", "frozen", "hang", "unresponsive", "cannot use", "blocked", "broken"
|
||||
- Does it prevent core functionality? Can users work around it?
|
||||
- Be conservative - only flag issues that truly prevent users from getting work done
|
||||
|
||||
4. For issues that are truly blocking and don't already have the "oncall" label:
|
||||
- Use `gh issue edit <number> --repo anthropics/claude-code --add-label "oncall"`
|
||||
- Mark the issue as complete in your TODO list
|
||||
|
||||
5. After processing all issues, provide a summary:
|
||||
- List each issue number that received the "oncall" label
|
||||
- Include the issue title and brief reason why it qualified
|
||||
- If no issues qualified, state that clearly
|
||||
|
||||
Important:
|
||||
- Process ALL issues in your TODO list systematically
|
||||
- Don't post any comments to issues
|
||||
- Only add the "oncall" label, never remove it
|
||||
- Use individual `gh issue view` commands instead of bash for loops to avoid approval prompts
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
allowed-tools: Bash(./scripts/gh.sh:*),Bash(./scripts/edit-issue-labels.sh:*)
|
||||
description: Triage GitHub issues by analyzing and applying labels
|
||||
---
|
||||
|
||||
You're an issue triage assistant. Analyze the issue and manage labels.
|
||||
|
||||
IMPORTANT: Don't post any comments or messages to the issue. Your only actions are adding or removing labels.
|
||||
|
||||
Context:
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
TOOLS:
|
||||
- `./scripts/gh.sh` — wrapper for `gh` CLI. Only supports these subcommands and flags:
|
||||
- `./scripts/gh.sh label list` — fetch all available labels
|
||||
- `./scripts/gh.sh label list --limit 100` — fetch with limit
|
||||
- `./scripts/gh.sh issue view 123` — read issue title, body, and labels
|
||||
- `./scripts/gh.sh issue view 123 --comments` — read the conversation
|
||||
- `./scripts/gh.sh issue list --state open --limit 20` — list issues
|
||||
- `./scripts/gh.sh search issues "query"` — find similar or duplicate issues
|
||||
- `./scripts/gh.sh search issues "query" --limit 10` — search with limit
|
||||
- `./scripts/edit-issue-labels.sh --add-label LABEL --remove-label LABEL` — add or remove labels (issue number is read from the workflow event)
|
||||
|
||||
TASK:
|
||||
|
||||
1. Run `./scripts/gh.sh label list` to fetch the available labels. You may ONLY use labels from this list. Never invent new labels.
|
||||
2. Run `./scripts/gh.sh issue view ISSUE_NUMBER` to read the issue details.
|
||||
3. Run `./scripts/gh.sh issue view ISSUE_NUMBER --comments` to read the conversation.
|
||||
|
||||
**If EVENT is "issues" (new issue):**
|
||||
|
||||
4. First, check if this issue is actually about Claude Code.
|
||||
- Look for Claude Code signals in the issue BODY: a `Claude Code Version` field or `claude --version` output, references to the `claude` CLI command, terminal sessions, the VS Code/JetBrains extensions, `CLAUDE.md` files, `.claude/` directories, MCP servers, Cowork, Remote Control, or the web UI at claude.ai/code. If ANY such signal is present, this IS a Claude Code issue — proceed to step 5.
|
||||
- Only if NO Claude Code signals are present: check whether a different Anthropic product (claude.ai chat, Claude Desktop/Mobile apps, the raw Anthropic API/SDK, or account billing with no CLI involvement) is the *subject* of the complaint, not merely mentioned for context. If so, apply `invalid` and stop. If ambiguous, proceed to step 5 WITHOUT applying `invalid`.
|
||||
- The body text is authoritative. If a form dropdown (e.g. Platform) contradicts evidence in the body, trust the body — dropdowns are often mis-selected.
|
||||
|
||||
5. Analyze and apply category labels:
|
||||
- Type (bug, enhancement, question, etc.)
|
||||
- Technical areas and platform
|
||||
- Check for duplicates with `./scripts/gh.sh search issues`. Only mark as duplicate of OPEN issues.
|
||||
|
||||
6. Evaluate lifecycle labels:
|
||||
- `needs-repro` (bugs only, 7 days): Bug reports without clear steps to reproduce. A good repro has specific, followable steps that someone else could use to see the same issue.
|
||||
Do NOT apply if the user already provided error messages, logs, file paths, or a description of what they did. Don't require a specific format — narrative descriptions count.
|
||||
For model behavior issues (e.g. "Claude does X when it should do Y"), don't require traditional repro steps — examples and patterns are sufficient.
|
||||
- `needs-info` (bugs only, 7 days): The issue needs something from the community before it can progress — e.g. error messages, versions, environment details, or answers to follow-up questions. Don't apply to questions or enhancements.
|
||||
Do NOT apply if the user already provided version, environment, and error details. If the issue just needs engineering investigation, that's not `needs-info`.
|
||||
|
||||
Issues with these labels are automatically closed after the timeout if there's no response.
|
||||
The goal is to avoid issues lingering without a clear next step.
|
||||
|
||||
7. Apply all selected labels:
|
||||
`./scripts/edit-issue-labels.sh --add-label "label1" --add-label "label2"`
|
||||
|
||||
**If EVENT is "issue_comment" (comment on existing issue):**
|
||||
|
||||
4. Evaluate lifecycle labels based on the full conversation:
|
||||
- If the issue has `stale` or `autoclose`, remove the label — a new human comment means the issue is still active:
|
||||
`./scripts/edit-issue-labels.sh --remove-label "stale" --remove-label "autoclose"`
|
||||
- If the issue has `needs-repro` or `needs-info` and the missing information has now been provided, remove the label:
|
||||
`./scripts/edit-issue-labels.sh --remove-label "needs-repro"`
|
||||
- If the issue doesn't have lifecycle labels but clearly needs them (e.g., a maintainer asked for repro steps or more details), add the appropriate label.
|
||||
- Comments like "+1", "me too", "same here", or emoji reactions are NOT the missing information. Only remove `needs-repro` or `needs-info` when substantive details are actually provided.
|
||||
- Do NOT add or remove category labels (bug, enhancement, etc.) on comment events.
|
||||
|
||||
GUIDELINES:
|
||||
- ONLY use labels from `./scripts/gh.sh label list` — never create or guess label names
|
||||
- DO NOT post any comments to the issue
|
||||
- Be conservative with lifecycle labels — only apply when clearly warranted
|
||||
- Only apply lifecycle labels (`needs-repro`, `needs-info`) to bugs — never to questions or enhancements
|
||||
- When in doubt, don't apply a lifecycle label — false positives are worse than missing labels
|
||||
- On new issues (EVENT "issues"), always apply exactly one of `bug`, `enhancement`, `question`, `invalid`, or `duplicate`. If unsure, pick the closest fit — an imperfect category label is better than none.
|
||||
- On comment events, it's okay to make no changes if nothing applies.
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 (sha-pinned)
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 (sha-pinned)
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
|
||||
@@ -17,28 +17,40 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
# Required to mint the OIDC token exchanged for a Claude API access token (Workload Identity Federation)
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Claude Code slash command
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-action@v1
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CLAUDE_CODE_SCRIPT_CAPS: '{"comment-on-duplicates.sh":1}'
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
prompt: "/dedupe ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Authenticate to the Claude API via Workload Identity Federation
|
||||
# (the workflow's OIDC token is exchanged for a short-lived access
|
||||
# token) instead of a static API key.
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
anthropic_workspace_id: ${{ vars.ANTHROPIC_WORKSPACE_ID }}
|
||||
claude_args: "--model claude-sonnet-4-5-20250929"
|
||||
claude_env: |
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log duplicate comment event to Statsig
|
||||
if: always()
|
||||
env:
|
||||
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }}
|
||||
REPO: ${{ github.repository }}
|
||||
TRIGGERED_BY: ${{ github.event_name }}
|
||||
WORKFLOW_RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
ISSUE_NUMBER=${{ github.event.issue.number || inputs.issue_number }}
|
||||
REPO=${{ github.repository }}
|
||||
|
||||
if [ -z "$STATSIG_API_KEY" ]; then
|
||||
echo "STATSIG_API_KEY not found, skipping Statsig logging"
|
||||
exit 0
|
||||
@@ -48,7 +60,8 @@ jobs:
|
||||
EVENT_PAYLOAD=$(jq -n \
|
||||
--arg issue_number "$ISSUE_NUMBER" \
|
||||
--arg repo "$REPO" \
|
||||
--arg triggered_by "${{ github.event_name }}" \
|
||||
--arg triggered_by "$TRIGGERED_BY" \
|
||||
--arg workflow_run_id "$WORKFLOW_RUN_ID" \
|
||||
'{
|
||||
events: [{
|
||||
eventName: "github_duplicate_comment_added",
|
||||
@@ -57,7 +70,7 @@ jobs:
|
||||
repository: $repo,
|
||||
issue_number: ($issue_number | tonumber),
|
||||
triggered_by: $triggered_by,
|
||||
workflow_run_id: "${{ github.run_id }}"
|
||||
workflow_run_id: $workflow_run_id
|
||||
},
|
||||
time: (now | floor | tostring)
|
||||
}]
|
||||
|
||||
@@ -1,107 +1,47 @@
|
||||
name: Claude Issue Triage
|
||||
description: Automatically triage GitHub issues using Claude Code
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
triage-issue:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
if: >-
|
||||
github.event_name == 'issues' ||
|
||||
(github.event_name == 'issue_comment' && !github.event.issue.pull_request && github.event.comment.user.type != 'Bot')
|
||||
concurrency:
|
||||
group: issue-triage-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
# Required to mint the OIDC token exchanged for a Claude API access token (Workload Identity Federation)
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create triage prompt
|
||||
run: |
|
||||
mkdir -p /tmp/claude-prompts
|
||||
cat > /tmp/claude-prompts/triage-prompt.txt << 'EOF'
|
||||
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
|
||||
|
||||
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
|
||||
|
||||
Issue Information:
|
||||
- REPO: ${{ github.repository }}
|
||||
- ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
|
||||
TASK OVERVIEW:
|
||||
|
||||
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
||||
|
||||
2. Next, use the GitHub tools to get context about the issue:
|
||||
- You have access to these tools:
|
||||
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
|
||||
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
|
||||
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
|
||||
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
|
||||
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
|
||||
- Start by using mcp__github__get_issue to get the issue details
|
||||
|
||||
3. Analyze the issue content, considering:
|
||||
- The issue title and description
|
||||
- The type of issue (bug report, feature request, question, etc.)
|
||||
- Technical areas mentioned
|
||||
- Severity or priority indicators
|
||||
- User impact
|
||||
- Components affected
|
||||
|
||||
4. Select appropriate labels from the available labels list provided above:
|
||||
- Choose labels that accurately reflect the issue's nature
|
||||
- Be specific but comprehensive
|
||||
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
|
||||
- Consider platform labels (android, ios) if applicable
|
||||
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
||||
|
||||
5. Apply the selected labels:
|
||||
- Use mcp__github__update_issue to apply your selected labels
|
||||
- DO NOT post any comments explaining your decision
|
||||
- DO NOT communicate directly with users
|
||||
- If no labels are clearly applicable, do not apply any labels
|
||||
|
||||
IMPORTANT GUIDELINES:
|
||||
- Be thorough in your analysis
|
||||
- Only select labels from the provided list above
|
||||
- DO NOT post any comments to the issue
|
||||
- Your ONLY action should be to apply labels using mcp__github__update_issue
|
||||
- It's okay to not add any labels if none are clearly applicable
|
||||
EOF
|
||||
|
||||
- name: Setup GitHub MCP Server
|
||||
run: |
|
||||
mkdir -p /tmp/mcp-config
|
||||
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Run Claude Code for Issue Triage
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
timeout-minutes: 5
|
||||
uses: anthropics/claude-code-action@v1
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}'
|
||||
with:
|
||||
prompt_file: /tmp/claude-prompts/triage-prompt.txt
|
||||
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
||||
timeout_minutes: "5"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
mcp_config: /tmp/mcp-config/mcp-servers.json
|
||||
claude_args: "--model claude-sonnet-4-5-20250929"
|
||||
claude_env: |
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
prompt: "/triage-issue REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }} EVENT: ${{ github.event_name }}"
|
||||
# Authenticate to the Claude API via Workload Identity Federation
|
||||
# (the workflow's OIDC token is exchanged for a short-lived access
|
||||
# token) instead of a static API key.
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
anthropic_workspace_id: ${{ vars.ANTHROPIC_WORKSPACE_ID }}
|
||||
claude_args: |
|
||||
--model claude-opus-4-6
|
||||
|
||||
@@ -31,8 +31,14 @@ jobs:
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Authenticate to the Claude API via Workload Identity Federation
|
||||
# (the workflow's OIDC token is exchanged for a short-lived access
|
||||
# token) instead of a static API key.
|
||||
anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
|
||||
anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
|
||||
anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
|
||||
anthropic_workspace_id: ${{ vars.ANTHROPIC_WORKSPACE_ID }}
|
||||
claude_args: "--model claude-sonnet-4-5-20250929"
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: "Issue Lifecycle Comment"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 (sha-pinned)
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Post lifecycle comment
|
||||
run: bun run scripts/lifecycle-comment.ts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LABEL: ${{ github.event.label.name }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
@@ -22,71 +22,56 @@ jobs:
|
||||
script: |
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
const cutoff = sevenDaysAgo.toISOString().split('T')[0];
|
||||
|
||||
const lockComment = `This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.`;
|
||||
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
const query = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:closed is:unlocked updated:<${cutoff}`;
|
||||
console.log(`Search query: ${query}`);
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const MAX_PER_RUN = 250;
|
||||
const processed = new Set();
|
||||
let totalLocked = 0;
|
||||
|
||||
while (hasMore) {
|
||||
// Get closed issues (pagination)
|
||||
const { data: issues } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed',
|
||||
while (totalLocked < MAX_PER_RUN) {
|
||||
const { data } = await github.rest.search.issuesAndPullRequests({
|
||||
q: query,
|
||||
sort: 'updated',
|
||||
direction: 'asc',
|
||||
order: 'asc',
|
||||
per_page: 100,
|
||||
page: page
|
||||
});
|
||||
|
||||
if (issues.length === 0) {
|
||||
hasMore = false;
|
||||
break;
|
||||
|
||||
if (totalLocked === 0) {
|
||||
console.log(`Total candidates: ${data.total_count}`);
|
||||
}
|
||||
|
||||
for (const issue of issues) {
|
||||
// Skip if already locked
|
||||
if (issue.locked) continue;
|
||||
|
||||
// Skip pull requests
|
||||
if (issue.pull_request) continue;
|
||||
|
||||
// Check if updated more than 7 days ago
|
||||
const updatedAt = new Date(issue.updated_at);
|
||||
if (updatedAt > sevenDaysAgo) {
|
||||
// Since issues are sorted by updated_at ascending,
|
||||
// once we hit a recent issue, all remaining will be recent too
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
const fresh = data.items.filter((i) => !processed.has(i.number));
|
||||
if (fresh.length === 0) break;
|
||||
|
||||
for (const issue of fresh) {
|
||||
if (totalLocked >= MAX_PER_RUN) break;
|
||||
processed.add(issue.number);
|
||||
try {
|
||||
// Add comment before locking
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: lockComment
|
||||
body: lockComment,
|
||||
});
|
||||
|
||||
// Lock the issue
|
||||
await github.rest.issues.lock({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
lock_reason: 'resolved'
|
||||
lock_reason: 'resolved',
|
||||
});
|
||||
|
||||
totalLocked++;
|
||||
console.log(`Locked issue #${issue.number}: ${issue.title}`);
|
||||
await sleep(1000);
|
||||
} catch (error) {
|
||||
console.error(`Failed to lock issue #${issue.number}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
console.log(`Total issues locked: ${totalLocked}`);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Non-write Users Check
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
allowed-non-write-check:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- run: |
|
||||
DIFF=$(gh pr diff "$PR_NUMBER" -R "$REPO" || true)
|
||||
|
||||
if ! echo "$DIFF" | grep -qE '^diff --git a/\.github/.*\.ya?ml'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MATCHES=$(echo "$DIFF" | grep "^+.*allowed_non_write_users" || true)
|
||||
|
||||
if [ -z "$MATCHES" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
EXISTING=$(gh pr view "$PR_NUMBER" -R "$REPO" --json comments --jq '.comments[].body' \
|
||||
| grep -c "<!-- non-write-users-check -->" || true)
|
||||
|
||||
if [ "$EXISTING" -gt 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh pr comment "$PR_NUMBER" -R "$REPO" --body '<!-- non-write-users-check -->
|
||||
**`allowed_non_write_users` detected**
|
||||
|
||||
This PR adds or modifies `allowed_non_write_users`, which allows users without write access to trigger Claude Code Action workflows. This can introduce security risks.
|
||||
|
||||
If this is a new flow, please make sure you actually need `allowed_non_write_users`. If you are editing an existing workflow, double check that you are not adding new Claude permissions which might lead to a vulnerability.
|
||||
|
||||
See existing workflows in this repo for safe usage examples, or contact the AppSec team.'
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
@@ -1,120 +0,0 @@
|
||||
name: Oncall Issue Triage
|
||||
description: Automatically identify and label critical blocking issues requiring oncall attention
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- add-oncall-triage-workflow # Temporary: for testing only
|
||||
schedule:
|
||||
# Run every 6 hours
|
||||
- cron: '0 */6 * * *'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
oncall-triage:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create oncall triage prompt
|
||||
run: |
|
||||
mkdir -p /tmp/claude-prompts
|
||||
cat > /tmp/claude-prompts/oncall-triage-prompt.txt << 'EOF'
|
||||
You're an oncall triage assistant for GitHub issues. Your task is to identify critical issues that require immediate oncall attention.
|
||||
|
||||
Important: Don't post any comments or messages to the issues. Your only action should be to apply the "oncall" label to qualifying issues.
|
||||
|
||||
Repository: ${{ github.repository }}
|
||||
|
||||
Task overview:
|
||||
1. Fetch all open issues updated in the last 3 days:
|
||||
- Use mcp__github__list_issues with:
|
||||
- state="open"
|
||||
- first=5 (fetch only 5 issues per page)
|
||||
- orderBy="UPDATED_AT"
|
||||
- direction="DESC"
|
||||
- This will give you the most recently updated issues first
|
||||
- For each page of results, check the updatedAt timestamp of each issue
|
||||
- Add issues updated within the last 3 days (72 hours) to your TODO list as you go
|
||||
- Keep paginating using the 'after' parameter until you encounter issues older than 3 days
|
||||
- Once you hit issues older than 3 days, you can stop fetching (no need to fetch all open issues)
|
||||
|
||||
2. Build your TODO list incrementally as you fetch:
|
||||
- As you fetch each page, immediately add qualifying issues to your TODO list
|
||||
- One TODO item per issue number (e.g., "Evaluate issue #123")
|
||||
- This allows you to start processing while still fetching more pages
|
||||
|
||||
3. For each issue in your TODO list:
|
||||
- Use mcp__github__get_issue to read the issue details (title, body, labels)
|
||||
- Use mcp__github__get_issue_comments to read all comments
|
||||
- Evaluate whether this issue needs the oncall label:
|
||||
a) Is it a bug? (has "bug" label or describes bug behavior)
|
||||
b) Does it have at least 50 engagements? (count comments + reactions)
|
||||
c) Is it truly blocking? Read and understand the full content to determine:
|
||||
- Does this prevent core functionality from working?
|
||||
- Can users work around it?
|
||||
- Consider severity indicators: "crash", "stuck", "frozen", "hang", "unresponsive", "cannot use", "blocked", "broken"
|
||||
- Be conservative - only flag issues that truly prevent users from getting work done
|
||||
|
||||
4. For issues that meet all criteria and do not already have the "oncall" label:
|
||||
- Use mcp__github__update_issue to add the "oncall" label
|
||||
- Do not post any comments
|
||||
- Do not remove any existing labels
|
||||
- Do not remove the "oncall" label from issues that already have it
|
||||
|
||||
Important guidelines:
|
||||
- Use the TODO list to track your progress through ALL candidate issues
|
||||
- Process issues efficiently - don't read every single issue upfront, work through your TODO list systematically
|
||||
- Be conservative in your assessment - only flag truly critical blocking issues
|
||||
- Do not post any comments to issues
|
||||
- Your only action should be to add the "oncall" label using mcp__github__update_issue
|
||||
- Mark each issue as complete in your TODO list as you process it
|
||||
|
||||
7. After processing all issues in your TODO list, provide a summary of your actions:
|
||||
- Total number of issues processed (candidate issues evaluated)
|
||||
- Number of issues that received the "oncall" label
|
||||
- For each issue that got the label: list issue number, title, and brief reason why it qualified
|
||||
- Close calls: List any issues that almost qualified but didn't quite meet the criteria (e.g., borderline blocking, had workarounds)
|
||||
- If no issues qualified, state that clearly
|
||||
- Format the summary clearly for easy reading
|
||||
EOF
|
||||
|
||||
- name: Setup GitHub MCP Server
|
||||
run: |
|
||||
mkdir -p /tmp/mcp-config
|
||||
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Run Claude Code for Oncall Triage
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
with:
|
||||
prompt_file: /tmp/claude-prompts/oncall-triage-prompt.txt
|
||||
allowed_tools: "mcp__github__list_issues,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue"
|
||||
timeout_minutes: "10"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
mcp_config: /tmp/mcp-config/mcp-servers.json
|
||||
claude_env: |
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,42 @@
|
||||
name: "Remove Autoclose Label on Activity"
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
remove-autoclose:
|
||||
# Only run if the issue has the autoclose label
|
||||
if: |
|
||||
github.event.issue.state == 'open' &&
|
||||
contains(github.event.issue.labels.*.name, 'autoclose') &&
|
||||
github.event.comment.user.login != 'github-actions[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Remove autoclose label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
console.log(`Removing autoclose label from issue #${context.issue.number} due to new comment from ${context.payload.comment.user.login}`);
|
||||
|
||||
try {
|
||||
// Remove the autoclose label
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'autoclose'
|
||||
});
|
||||
|
||||
console.log(`Successfully removed autoclose label from issue #${context.issue.number}`);
|
||||
} catch (error) {
|
||||
// If the label was already removed or doesn't exist, that's fine
|
||||
if (error.status === 404) {
|
||||
console.log(`Autoclose label was already removed from issue #${context.issue.number}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
name: "Issue Sweep"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 10,22 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: daily-issue-sweep
|
||||
|
||||
jobs:
|
||||
sweep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 (sha-pinned)
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Enforce lifecycle timeouts
|
||||
run: bun run scripts/sweep.ts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
|
||||
+4369
-5
File diff suppressed because it is too large
Load Diff
@@ -6,17 +6,42 @@
|
||||
|
||||
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows -- all through natural language commands. Use it in your terminal, IDE, or tag @claude on Github.
|
||||
|
||||
**Learn more in the [official documentation](https://docs.anthropic.com/en/docs/claude-code/overview)**.
|
||||
**Learn more in the [official documentation](https://code.claude.com/docs/en/overview)**.
|
||||
|
||||
<img src="./demo.gif" />
|
||||
|
||||
## Get started
|
||||
> [!NOTE]
|
||||
> Installation via npm is deprecated. Use one of the recommended methods below.
|
||||
|
||||
For more installation options, uninstall steps, and troubleshooting, see the [setup documentation](https://code.claude.com/docs/en/setup).
|
||||
|
||||
1. Install Claude Code:
|
||||
|
||||
```sh
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
```
|
||||
**MacOS/Linux (Recommended):**
|
||||
```bash
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
```
|
||||
|
||||
**Homebrew (MacOS/Linux):**
|
||||
```bash
|
||||
brew install --cask claude-code
|
||||
```
|
||||
|
||||
**Windows (Recommended):**
|
||||
```powershell
|
||||
irm https://claude.ai/install.ps1 | iex
|
||||
```
|
||||
|
||||
**WinGet (Windows):**
|
||||
```powershell
|
||||
winget install Anthropic.ClaudeCode
|
||||
```
|
||||
|
||||
**NPM (Deprecated):**
|
||||
```bash
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
```
|
||||
|
||||
2. Navigate to your project directory and run `claude`.
|
||||
|
||||
@@ -38,7 +63,7 @@ When you use Claude Code, we collect feedback, which includes usage data (such a
|
||||
|
||||
### How we use your data
|
||||
|
||||
See our [data usage policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
See our [data usage policies](https://code.claude.com/docs/en/data-usage).
|
||||
|
||||
### Privacy safeguards
|
||||
|
||||
|
||||
+3
-3
@@ -5,8 +5,8 @@ Thank you for helping us keep Claude Code secure!
|
||||
|
||||
The security of our systems and user data is Anthropic's top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities.
|
||||
|
||||
Our security program is managed on HackerOne and we ask that any validated vulnerability in this functionality be reported through their [submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability).
|
||||
Our security program is managed on HackerOne and we ask that any validated vulnerability in this functionality be reported through their [submission form](https://hackerone.com/4f1f16ba-10d3-4d09-9ecc-c721aad90f24/embedded_submissions/new).
|
||||
|
||||
## Vulnerability Disclosure Program
|
||||
## Anthropic Bug Bounty
|
||||
|
||||
Our Vulnerability Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp).
|
||||
Our Bug Bounty Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic).
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Keep secrets and generated artifacts out of the build context. The Dockerfile
|
||||
# COPYs the binary, gateway.yaml (unlike the GCP example, the config is baked
|
||||
# into the image — ECS injects only the secrets it references, as env vars),
|
||||
# and the RDS CA bundle. BuildKit (the default builder) only syncs the
|
||||
# referenced COPY sources anyway, so this is a denylist for the classic
|
||||
# builder (DOCKER_BUILDKIT=0) and a conventional signal that the .gitignore'd
|
||||
# secrets in this directory aren't part of the image build.
|
||||
terraform/
|
||||
**/.terraform/
|
||||
*.tfstate*
|
||||
terraform.tfvars
|
||||
secrets/
|
||||
*.pem
|
||||
# The RDS CA bundle is public trust-anchor material (no secret), and the
|
||||
# Dockerfile COPYs it — carve it out of the *.pem exclusion above.
|
||||
!rds-global-bundle.pem
|
||||
*.iam.json
|
||||
claude.download
|
||||
claude.bad
|
||||
@@ -0,0 +1,18 @@
|
||||
# Local, environment-specific config — copy gateway.yaml.example -> gateway.yaml
|
||||
# (gateway.yaml.example IS committed; your filled-in gateway.yaml is not)
|
||||
gateway.yaml
|
||||
|
||||
# Secrets / credentials — never commit. Also covers rds-global-bundle.pem:
|
||||
# not a secret, but downloaded by setup.sh when absent (delete it to refresh
|
||||
# after an RDS CA rotation), so it stays out of git.
|
||||
secrets/
|
||||
*.pem
|
||||
|
||||
# Scratch IAM policy documents written by setup.sh (no secrets, but generated)
|
||||
*.iam.json
|
||||
|
||||
# Vendored release binary — download per release (see setup.sh DIST_URL).
|
||||
# claude.bad is a checksum-mismatched binary that setup.sh set aside.
|
||||
claude
|
||||
claude.download
|
||||
claude.bad
|
||||
@@ -0,0 +1,67 @@
|
||||
# Runtime image for `claude gateway`.
|
||||
#
|
||||
# This image does NOT build the binary. It expects a prebuilt native
|
||||
# linux-x64 `claude` executable in the build context — the Claude Code release
|
||||
# binary, which includes the `gateway` subcommand. setup.sh places it at
|
||||
# ./claude (downloading and checksum-verifying it via DIST_URL/DIST_SHA256 if
|
||||
# missing). Override CLAUDE_BINARY to point at a different path.
|
||||
#
|
||||
# Unlike the GCP example (which mounts the config from Secret Manager at
|
||||
# runtime), this image BAKES gateway.yaml in at /etc/claude/gateway.yaml — on
|
||||
# ECS the task definition injects only the secrets the YAML references, as env
|
||||
# vars. gateway.yaml therefore must be fully filled in (no REPLACE_ME) before
|
||||
# building; setup.sh enforces this. A config edit means a rebuild under a new
|
||||
# tag. The file contains no secret values — every credential resolves at boot
|
||||
# via ${ENV_VAR} expansion.
|
||||
#
|
||||
# The image also bakes in the AWS RDS CA bundle (rds-global-bundle.pem —
|
||||
# setup.sh downloads it from https://truststore.pki.rds.amazonaws.com before
|
||||
# the build) and trusts it via NODE_EXTRA_CA_CERTS, so the store connection
|
||||
# string's `?sslmode=verify-full` verifies the RDS server certificate chain
|
||||
# and hostname. NOTE the gateway's driver reads `sslmode` from the URL but NOT
|
||||
# a libpq-style `sslrootcert=` param — the CA must come from this env var.
|
||||
#
|
||||
# Build:
|
||||
# docker build --platform=linux/amd64 --provenance=false \
|
||||
# --build-arg CLAUDE_BINARY=./claude -t claude-gateway .
|
||||
#
|
||||
# (For Fargate on ARM64/Graviton: build --platform=linux/arm64 with the
|
||||
# linux-arm64 binary and set the task definition's cpuArchitecture to ARM64.)
|
||||
#
|
||||
# Run:
|
||||
# docker run --rm -p 8080:8080 \
|
||||
# -e OIDC_CLIENT_SECRET -e GATEWAY_JWT_SECRET -e GATEWAY_POSTGRES_URL \
|
||||
# claude-gateway
|
||||
|
||||
ARG CLAUDE_BINARY=./claude
|
||||
ARG GATEWAY_CONFIG=./gateway.yaml
|
||||
ARG RDS_CA_BUNDLE=./rds-global-bundle.pem
|
||||
|
||||
# distroless/cc provides glibc + libstdc++ (required by the Bun-compiled
|
||||
# native binary). The :nonroot tag runs as uid/gid 65532. Pinned by digest so
|
||||
# the build never silently takes new upstream bytes (the digest is the
|
||||
# multi-arch OCI index, so --platform still selects amd64/arm64). To refresh
|
||||
# the pin after reviewing upstream changes:
|
||||
# docker manifest inspect -v gcr.io/distroless/cc-debian12:nonroot # prints the index digest
|
||||
FROM gcr.io/distroless/cc-debian12:nonroot@sha256:ce0d66bc0f64aae46e6a03add867b07f42cc7b8799c949c2e898057b7f75a151
|
||||
|
||||
ARG CLAUDE_BINARY
|
||||
ARG GATEWAY_CONFIG
|
||||
ARG RDS_CA_BUNDLE
|
||||
COPY --chmod=0755 ${CLAUDE_BINARY} /usr/local/bin/claude
|
||||
# WORKDIR pre-creates /etc/claude with 0755 — without it, COPY --chmod would
|
||||
# also stamp the auto-created parent directory 0644 (no execute bit), making
|
||||
# the config unreadable for the nonroot user.
|
||||
WORKDIR /etc/claude
|
||||
COPY --chmod=0644 ${GATEWAY_CONFIG} /etc/claude/gateway.yaml
|
||||
COPY --chmod=0644 ${RDS_CA_BUNDLE} /etc/claude/rds-global-bundle.pem
|
||||
WORKDIR /
|
||||
|
||||
ENV CLAUDE_CONFIG_DIR=/tmp/.claude
|
||||
# Trust anchor for the store's sslmode=verify-full (see header comment).
|
||||
ENV NODE_EXTRA_CA_CERTS=/etc/claude/rds-global-bundle.pem
|
||||
|
||||
EXPOSE 8080
|
||||
USER nonroot
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/claude", "gateway", "--config", "/etc/claude/gateway.yaml"]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Claude apps gateway on AWS
|
||||
|
||||
Reference deployment artifacts for running Claude apps gateway on AWS with
|
||||
Amazon Bedrock as the upstream: ECS on Fargate or EKS, Amazon RDS for
|
||||
PostgreSQL, AWS Secrets Manager, and IAM-role auth to Bedrock.
|
||||
|
||||
These files are provided as a working example rather than a supported production
|
||||
deployment. Adapt them to your own environment.
|
||||
|
||||
- **Walkthrough**: https://code.claude.com/docs/en/claude-apps-gateway-on-aws
|
||||
- **Related**: AWS-maintained samples for various customer environments at
|
||||
https://github.com/aws-samples/anthropic-on-aws/tree/main/claude-apps-gateway
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `setup.sh` | Scripts the walkthrough end to end via the `aws` CLI |
|
||||
| `Dockerfile` | Runtime image for the `claude gateway` binary (bakes in `gateway.yaml`) |
|
||||
| `gateway.yaml.example` | Gateway config template, AWS-shaped (Bedrock upstream, Okta IdP) |
|
||||
| `terraform/` | Provisions the full architecture (two-pass apply — see `terraform/README.md`) |
|
||||
@@ -0,0 +1,174 @@
|
||||
# gateway.yaml.example — Claude apps gateway config template, AWS-shaped (walkthrough §4).
|
||||
#
|
||||
# Okta IdP + Bedrock upstream, following the walkthrough at
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-on-aws. The active sections
|
||||
# below are a strict subset of the full configuration reference at
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-config; optional keys are
|
||||
# included commented-out.
|
||||
#
|
||||
# USAGE — this is the shippable TEMPLATE. Copy it to gateway.yaml and fill it in:
|
||||
# cp gateway.yaml.example gateway.yaml
|
||||
# setup.sh and terraform/ read gateway.yaml (your filled-in copy, which is
|
||||
# gitignored). Unlike the GCP example it is NOT published to a secret store:
|
||||
# the Dockerfile bakes it into the image at /etc/claude/gateway.yaml — the
|
||||
# container ENTRYPOINT runs `claude gateway --config /etc/claude/gateway.yaml`.
|
||||
# It holds no secret values; a config edit means an image rebuild (setup.sh
|
||||
# tags images with a hash of this file, so a re-run rebuilds automatically).
|
||||
#
|
||||
# Secret expansion: ${ENV_VAR} reads an env var; ${file:/path} reads a mounted file.
|
||||
# On ECS, the task definition injects the JWT / OIDC / Postgres secrets as ENV
|
||||
# VARS via its `secrets` field (valueFrom -> Secrets Manager ARN). On EKS you
|
||||
# may mount them as files instead and use ${file:/secrets/...}.
|
||||
#
|
||||
# BEFORE BUILD — replace every REPLACE_ME placeholder below (setup.sh refuses to
|
||||
# build the image while any remain — the config is baked in, so a half-filled
|
||||
# config would ship), and create the referenced secrets:
|
||||
# gateway-jwt-secret (setup.sh generates this)
|
||||
# gateway-oidc-client-secret (from the Okta admin console OIDC web app)
|
||||
# gateway-postgres-url (setup.sh generates this)
|
||||
|
||||
# ── Listener ─────────────────────────────────────────────────────────────────
|
||||
listen:
|
||||
host: 0.0.0.0
|
||||
port: 8080 # the target group forwards ALB :443 -> :8080
|
||||
# Required. Fixes the IdP redirect_uri, the OIDC discovery doc, and the
|
||||
# gateway-token issuer so none are derived from the client-controlled Host
|
||||
# header (X-Forwarded-Host/-Proto are likewise never trusted). Set it to the
|
||||
# internal hostname you picked in the prerequisites — the Route 53 private
|
||||
# zone name your ACM certificate covers (e.g.
|
||||
# https://claude-gateway.internal.example.com). Unlike Cloud Run there is no
|
||||
# first-deploy placeholder dance: you choose the hostname up front, alias it
|
||||
# to the internal ALB after the deploy, and register the same host's
|
||||
# /oauth/callback on the Okta app.
|
||||
public_url: REPLACE_ME
|
||||
# Register this exact redirect URI on the Okta OIDC web application:
|
||||
# https://<public_url host>/oauth/callback
|
||||
#
|
||||
# Behind the internal ALB every request arrives via the load balancer, so the
|
||||
# gateway sees ALB-node peer IPs for all developers — set trusted_proxies so
|
||||
# X-Forwarded-For from those proxies is trusted and per-IP rate limiting /
|
||||
# audit IPs record the real client. ALB nodes take addresses from the subnets
|
||||
# the ALB is attached to, so list those subnets' CIDRs (the private subnets
|
||||
# from the prerequisites).
|
||||
#
|
||||
# NOTE: listing the ALB subnets' CIDRs trusts every host in those subnets as a
|
||||
# proxy — any co-located workload that can reach the ALB can then spoof the
|
||||
# client IP via X-Forwarded-For (audit logs, per-IP rate limits, IP
|
||||
# allowlists). Keep the ALB :443 ingress source (CORP_CIDR / corporate_cidr)
|
||||
# from overlapping these subnets, and don't share the subnets with untrusted
|
||||
# workloads.
|
||||
trusted_proxies: [REPLACE_ME] # e.g. [10.0.1.0/24, 10.0.2.0/24]
|
||||
#
|
||||
# Alternative — terminate TLS in the gateway itself instead of at the ALB:
|
||||
# tls:
|
||||
# cert: /certs/gateway.crt
|
||||
# key: /certs/gateway.key
|
||||
|
||||
# ── Identity provider — Okta ─────────────────────────────────────────────────
|
||||
oidc:
|
||||
issuer: REPLACE_ME # e.g. https://example.okta.com (or your custom auth server URL)
|
||||
client_id: REPLACE_ME # Okta OIDC web app client ID (not secret)
|
||||
client_secret: ${OIDC_CLIENT_SECRET} # EKS file mounts: ${file:/secrets/oidc-client-secret}
|
||||
allowed_email_domains: [REPLACE_ME] # e.g. [example.com] — reject id_tokens outside your org
|
||||
# The Okta org authorization server returns a thin id_token that omits email
|
||||
# and groups; the gateway fills them from /userinfo.
|
||||
userinfo_fallback: true
|
||||
# offline_access yields refresh tokens (silent renewal + the deprovision
|
||||
# leash); Okta emits groups only when the `groups` scope is requested AND the
|
||||
# app's groups claim filter allows them (Okta admin console -> the app's
|
||||
# Sign On tab -> OpenID Connect ID Token -> Groups claim filter).
|
||||
scopes: [openid, profile, email, offline_access, groups]
|
||||
# groups_claim: groups # Okta default. Entra app roles=roles; see the config reference
|
||||
# ca_cert_pem: ${file:/secrets/idp-ca.pem} # only for an IdP behind a private CA
|
||||
|
||||
# ── Sessions ─────────────────────────────────────────────────────────────────
|
||||
session:
|
||||
jwt_secret: ${GATEWAY_JWT_SECRET} # >= 32 bytes; openssl rand -base64 32
|
||||
# Okta issues refresh tokens (offline_access above), so sessions renew
|
||||
# silently and this mainly bounds deprovision latency. 8 is a sane default;
|
||||
# lower toward 1 for tighter revocation. Array form rotates keys:
|
||||
# [new, old] (index 0 signs, all verify).
|
||||
ttl_hours: 8
|
||||
|
||||
# ── Store (REQUIRED — the gateway refuses to boot without it) ─────────────────
|
||||
store:
|
||||
postgres_url: ${GATEWAY_POSTGRES_URL} # private-subnet RDS; built with ?sslmode=verify-full by setup.sh
|
||||
# (the image trusts the RDS CA bundle via NODE_EXTRA_CA_CERTS — see Dockerfile)
|
||||
|
||||
# ── Upstreams — Amazon Bedrock ───────────────────────────────────────────────
|
||||
upstreams:
|
||||
- provider: bedrock
|
||||
# Must equal the region you provision in (setup.sh's AWS_REGION /
|
||||
# terraform's region): the IAM policy's inference-profile ARNs are scoped
|
||||
# to that region, and Bedrock model access is enabled there (cross-region
|
||||
# us.anthropic.* profiles need access in every spanned region). NOTE: the
|
||||
# walkthrough is scoped to US regions — the built-in model catalog maps to
|
||||
# us.anthropic.* (US-geo) profiles; a non-US region also needs a models:
|
||||
# list below (see the model catalog section).
|
||||
region: REPLACE_ME # e.g. us-east-1
|
||||
auth: {} # AWS default credential chain: ECS task role / IRSA on EKS (preferred — no static keys)
|
||||
# base_url: https://bedrock-runtime.us-east-1.amazonaws.com # bedrock-runtime interface VPC endpoint, to keep model traffic off the public path
|
||||
# Add more upstreams for failover (tried top→bottom on 5xx/timeout/501): a
|
||||
# second region, or an anthropic/vertex fallback. See
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway.
|
||||
|
||||
# ── Telemetry fan-out (OPTIONAL) ─────────────────────────────────────────────
|
||||
# The CLI sends OTLP/HTTP to the gateway; the gateway fans out, stamping
|
||||
# user.id/user.email/user.groups server-side. On AWS, point at an OpenTelemetry
|
||||
# Collector (e.g. the AWS Distro for OpenTelemetry -> CloudWatch / Managed
|
||||
# Prometheus). When forward_to and public_url are both configured the gateway
|
||||
# pushes CLAUDE_CODE_ENABLE_TELEMETRY and the OTEL exporter selectors to every
|
||||
# client automatically — no per-developer config needed.
|
||||
# telemetry:
|
||||
# forward_to:
|
||||
# - url: https://otel-collector.internal.example.com:4318
|
||||
# headers:
|
||||
# Authorization: ${file:/secrets/otlp-token}
|
||||
# metrics: true # safe aggregate counters (default)
|
||||
# logs: false # carries bash commands / tool inputs — opt in deliberately
|
||||
# traces: false
|
||||
|
||||
# ── RBAC + managed settings (OPTIONAL; first-match-wins, top -> bottom) ───────
|
||||
# With Okta as IdP, match on the group names the `groups` scope emits (subject
|
||||
# to the app's groups claim filter), or on email_domain.
|
||||
# managed:
|
||||
# policies:
|
||||
# - match: { groups: [engineering] }
|
||||
# cli:
|
||||
# availableModels: [claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5]
|
||||
# permissions: { deny: ["Read(./.env)", "Read(./secrets/**)"] }
|
||||
# - match: {} # catch-all floor — keep LAST
|
||||
# cli:
|
||||
# availableModels: [claude-sonnet-4-6, claude-haiku-4-5]
|
||||
|
||||
# ── Admin API (OPTIONAL — enables db-mode runtime config + spend caps) ───────
|
||||
# admin_groups needs a groups claim — Okta provides one via the `groups` scope
|
||||
# above — or use the bootstrap keys below instead. Named keys for attribution
|
||||
# in the audit log; 32-char minimum on key values. On ECS add these to the task
|
||||
# definition's `secrets` field (valueFrom -> a Secrets Manager ARN), same as the
|
||||
# JWT/OIDC/Postgres secrets above; on EKS you may use ${file:...}.
|
||||
# admin:
|
||||
# write_keys:
|
||||
# - id: terraform
|
||||
# key: ${GATEWAY_ADMIN_WRITE_KEY}
|
||||
# read_keys:
|
||||
# - id: reporting
|
||||
# key: ${GATEWAY_ADMIN_READ_KEY}
|
||||
# # admin_groups: [platform-finops] # Okta group names via the groups scope
|
||||
|
||||
# ── Model catalog (OPTIONAL for US regions) ──────────────────────────────────
|
||||
# Default true: every built-in Claude model is exposed and auto-translated per
|
||||
# upstream (the built-in table already maps to us.anthropic.* cross-region
|
||||
# inference profiles). Set false + a models: list to pin IDs (e.g. an
|
||||
# application or provisioned-throughput inference-profile ARN).
|
||||
# NON-US REGIONS: the built-in us.anthropic.* mappings do not exist outside
|
||||
# the US geo — set auto_include_builtin_models: false and list your region's
|
||||
# inference profiles (eu.anthropic.*, apac.anthropic.*, ...) here, and widen
|
||||
# the geo prefix in the deploy's bedrock-invoke IAM policy to match. See the
|
||||
# models: guidance in the config reference:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-config
|
||||
# auto_include_builtin_models: true
|
||||
# models:
|
||||
# - id: claude-opus-4-8
|
||||
# label: Claude Opus 4.8
|
||||
# upstream_model: { bedrock: us.anthropic.claude-opus-4-8 }
|
||||
Executable
+964
@@ -0,0 +1,964 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# setup.sh — AWS setup for Claude apps gateway (walkthrough §1–7, ECS track).
|
||||
#
|
||||
# Provisions, in this order: the three security groups (§1), the task +
|
||||
# execution IAM roles (§2), the gateway container image in Amazon ECR (§6),
|
||||
# an RDS for PostgreSQL instance in the private subnets with no public
|
||||
# address (§3), the JWT + postgres-url secrets (§5), and an ECS Fargate
|
||||
# service behind an internal Application Load Balancer (§7).
|
||||
#
|
||||
# gateway.yaml (§4 of the walkthrough) is BAKED INTO THE IMAGE on this track —
|
||||
# the task definition injects only the secrets it references, as env vars — so
|
||||
# the config step here lives inside the image build (§6): the build is gated on
|
||||
# a fully filled-in gateway.yaml and the image tag carries a hash of it, so a
|
||||
# config edit triggers a rebuild on the next run.
|
||||
#
|
||||
# Section markers (§N) below map to the walkthrough:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-on-aws
|
||||
#
|
||||
# Covers here: security groups (§1) -> IAM roles + Bedrock model-access note (§2)
|
||||
# -> build & push image, config baked in (§6 + §4) -> DB subnet group
|
||||
# + RDS instance (§3) -> jwt + postgres-url secrets (§5) -> ECS
|
||||
# cluster/task definition/service + internal ALB (§7, ECS Fargate tab).
|
||||
# Not covered: EKS track (§7's EKS tab) — ECS Fargate is the lower-friction path here.
|
||||
# Bedrock model access (§2) — console-only; the script reminds you.
|
||||
# Route 53 alias — see the next steps it prints. Client MDM
|
||||
# push (§8) is covered by the walkthrough, not this script.
|
||||
#
|
||||
# Idempotent: existing resources are detected and skipped, so it is safe to re-run.
|
||||
# Reuse is by NAME, so a pre-existing resource may not match what this script
|
||||
# would have created: reuse that would change the exposure model is fatal (an
|
||||
# ALB that is not internal/in ${VPC_ID}); upsert-able settings are converged on
|
||||
# every run; other posture drift (extra security group ingress, a public or
|
||||
# unencrypted RDS instance, wrong-VPC target group) is checked and warned
|
||||
# about, never silently adopted.
|
||||
# Override any default below via environment variable, e.g. `AWS_REGION=us-west-2 ./setup.sh`.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- configuration (env-overridable) ----------------------------------------
|
||||
AWS_REGION="${AWS_REGION:-$(aws configure get region 2>/dev/null || true)}" # guide uses us-east-1 (a region where Bedrock serves the Claude models you need)
|
||||
ACCOUNT_ID="${ACCOUNT_ID:-$(aws sts get-caller-identity --query Account --output text 2>/dev/null || true)}"
|
||||
|
||||
VPC_ID="${VPC_ID:-}" # REQUIRED — the VPC from the prerequisites
|
||||
PRIVATE_SUBNETS="${PRIVATE_SUBNETS:-}" # REQUIRED — two+ private subnet IDs in different AZs, space-separated
|
||||
CORP_CIDR="${CORP_CIDR:-}" # REQUIRED — your corporate network CIDR (ALB :443 ingress source)
|
||||
# Must not overlap PRIVATE_SUBNETS: hosts there are trusted_proxies (gateway.yaml) and could spoof client IPs via X-Forwarded-For.
|
||||
|
||||
# §1 security groups
|
||||
ALB_SG_NAME="${ALB_SG_NAME:-claude-gateway-alb}"
|
||||
GW_SG_NAME="${GW_SG_NAME:-claude-gateway-svc}"
|
||||
DB_SG_NAME="${DB_SG_NAME:-claude-gateway-db}"
|
||||
|
||||
# §2 IAM roles (task role = the gateway's runtime AWS identity; execution role
|
||||
# = the ECS agent's identity for pulling the image and injecting secrets)
|
||||
TASK_ROLE="${TASK_ROLE:-claude-gateway-task}"
|
||||
EXEC_ROLE="${EXEC_ROLE:-claude-gateway-execution}"
|
||||
|
||||
# §6 image
|
||||
ECR_REPO="${ECR_REPO:-claude-gateway}" # ECR repository name
|
||||
VERSION="${VERSION:-}" # REQUIRED — the gateway release tag you build and push (e.g. the linux-x64 binary's version)
|
||||
DOCKERFILE="${DOCKERFILE:-./Dockerfile}"
|
||||
CLAUDE_BINARY="${CLAUDE_BINARY:-./claude}" # prebuilt linux-x64 Claude Code release binary (includes the gateway subcommand)
|
||||
DIST_URL="${DIST_URL:-}" # optional: download URL, used only if $CLAUDE_BINARY is missing
|
||||
DIST_SHA256="${DIST_SHA256:-}" # REQUIRED with DIST_URL: expected sha256 of the binary (verified fail-closed)
|
||||
DIST_SHA256="${DIST_SHA256,,}" # normalize to lowercase — openssl emits lowercase hex; some tools (PowerShell Get-FileHash) publish uppercase
|
||||
# Obtain DIST_SHA256 out-of-band — never from the server that serves DIST_URL.
|
||||
# For binaries from the standard Claude Code release channel, verify the
|
||||
# release's GPG-signed manifest.json and copy the platform checksum from it:
|
||||
# https://code.claude.com/docs/en/setup#binary-integrity-and-code-signing
|
||||
# For any other distribution channel, use the checksum published alongside the
|
||||
# download link on that channel.
|
||||
GATEWAY_YAML="${GATEWAY_YAML:-./gateway.yaml}" # §4 config file — BAKED into the image
|
||||
RDS_CA_BUNDLE="${RDS_CA_BUNDLE:-./rds-global-bundle.pem}" # RDS CA trust anchor — BAKED into the image (downloaded below if missing)
|
||||
# Official AWS RDS truststore. AWS rotates this bundle (new regional CAs get
|
||||
# appended), so no checksum is pinned — a pinned hash would break on every
|
||||
# rotation. The script downloads it only when absent (an existing file is never
|
||||
# re-downloaded); to pick up a rotation, delete the file — and since the image
|
||||
# tag hashes only gateway.yaml, also bump VERSION or set IMAGE_TAG so the
|
||||
# next run rebuilds rather than reusing the existing tag. Operators who want
|
||||
# to pin may pre-place a reviewed copy at ${RDS_CA_BUNDLE}.
|
||||
RDS_CA_BUNDLE_URL="${RDS_CA_BUNDLE_URL:-https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem}"
|
||||
REGISTRY="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
|
||||
# §3 RDS
|
||||
DB_SUBNET_GROUP="${DB_SUBNET_GROUP:-claude-gateway-db}"
|
||||
DB_PARAM_GROUP="${DB_PARAM_GROUP:-claude-gateway-db}" # carries rds.force_ssl=1 (server-side TLS enforcement)
|
||||
DB_INSTANCE="${DB_INSTANCE:-claude-gateway-db}"
|
||||
DB_CLASS="${DB_CLASS:-db.t4g.micro}"
|
||||
DB_STORAGE_GB="${DB_STORAGE_GB:-20}"
|
||||
DB_NAME="${DB_NAME:-claude_gateway}"
|
||||
DB_USER="${DB_USER:-gateway}"
|
||||
# PG14+ supported; 16 is the recommended default (matches terraform/'s).
|
||||
# Always pinned: the instance's engine version and the parameter group's
|
||||
# family must name the same major, so both derive from this one value.
|
||||
DB_ENGINE_VERSION="${DB_ENGINE_VERSION:-16}"
|
||||
|
||||
SECRET_NAME="${SECRET_NAME:-gateway-postgres-url}" # §5 store.postgres_url
|
||||
JWT_SECRET_NAME="${JWT_SECRET_NAME:-gateway-jwt-secret}" # §5 session.jwt_secret
|
||||
OIDC_SECRET_NAME="${OIDC_SECRET_NAME:-gateway-oidc-client-secret}" # operator-created (Okta OIDC web app)
|
||||
# NOTE: the execution role's secrets-read policy (§2) is built from these
|
||||
# three names, one per-secret ARN prefix each — a rename is picked up on the
|
||||
# next run (put-role-policy is an upsert).
|
||||
|
||||
# §7 ECS + internal ALB deploy
|
||||
CLUSTER="${CLUSTER:-claude-gateway}"
|
||||
SERVICE="${SERVICE:-claude-gateway}"
|
||||
TASK_FAMILY="${TASK_FAMILY:-claude-gateway}"
|
||||
LOG_GROUP="${LOG_GROUP:-/ecs/claude-gateway}"
|
||||
LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-90}" # CloudWatch retention — the group carries the gateway's audit events, so align with your audit retention policy
|
||||
ALB_NAME="${ALB_NAME:-claude-gateway}"
|
||||
TG_NAME="${TG_NAME:-claude-gateway}"
|
||||
# Explicit modern TLS policy — omitting it falls back to the legacy
|
||||
# ELBSecurityPolicy-2016-08 default, which still accepts TLS 1.0/1.1.
|
||||
ALB_SSL_POLICY="${ALB_SSL_POLICY:-ELBSecurityPolicy-TLS13-1-2-2021-06}"
|
||||
ACM_CERT_ARN="${ACM_CERT_ARN:-}" # REQUIRED for deploy — ACM cert for your internal gateway hostname
|
||||
TASK_CPU="${TASK_CPU:-1024}"
|
||||
TASK_MEMORY="${TASK_MEMORY:-2048}"
|
||||
DESIRED_COUNT="${DESIRED_COUNT:-1}" # each task opens a Postgres pool of up to 5 connections (store.max_connections default); keep DESIRED_COUNT × 5 below the DB class's max_connections (~80 on db.t4g.micro)
|
||||
DEPLOY="${DEPLOY:-1}" # set DEPLOY=0 to provision only, no ECS/ALB deploy
|
||||
|
||||
# ---- helpers ----------------------------------------------------------------
|
||||
log() { printf '\n==> %s\n' "$*"; }
|
||||
skip() { printf ' (exists) %s\n' "$*"; }
|
||||
curl_https() { curl --proto '=https' --proto-redir '=https' --tlsv1.2 "$@"; } # refuse plaintext/protocol-downgrade
|
||||
sha_of() { openssl dgst -sha256 "$1" | awk '{print $NF}'; } # openssl avoids shasum/sha256sum portability gaps
|
||||
|
||||
# authorize-security-group-ingress is NOT idempotent (re-adding a rule errors),
|
||||
# so tolerate exactly the duplicate-rule error and fail on anything else.
|
||||
authorize_ingress() {
|
||||
local out
|
||||
if out="$(aws ec2 authorize-security-group-ingress "$@" 2>&1)"; then
|
||||
return 0
|
||||
elif grep -q 'InvalidPermission.Duplicate' <<<"${out}"; then
|
||||
skip "ingress rule already present"
|
||||
else
|
||||
printf '%s\n' "${out}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Security-group lookup by name within the VPC; prints the GroupId or "None".
|
||||
sg_id() {
|
||||
aws ec2 describe-security-groups \
|
||||
--filters "Name=group-name,Values=$1" "Name=vpc-id,Values=${VPC_ID}" \
|
||||
--query 'SecurityGroups[0].GroupId' --output text 2>/dev/null || echo None
|
||||
}
|
||||
|
||||
# Name-based reuse can adopt a pre-existing group carrying ingress this script
|
||||
# never added. Audit after the intended rule is ensured: each group's traffic
|
||||
# path is exactly one rule (tcp <port> from <cidr-or-source-group>), so anything
|
||||
# else is flagged on stderr. Non-fatal — an extra rule may be a deliberate
|
||||
# operator addition — but every one widens the path, so it must be visible.
|
||||
warn_unexpected_ingress() { # <group-id> <group-name> <port> <expected cidr or source group-id>
|
||||
local perms
|
||||
if ! perms="$(aws ec2 describe-security-groups --group-ids "$1" \
|
||||
--query 'SecurityGroups[0].IpPermissions' --output json 2>/dev/null)"; then
|
||||
echo " WARN — could not audit ingress rules on $2 ($1)." >&2
|
||||
return 0
|
||||
fi
|
||||
# `|| echo` keeps a parse hiccup non-fatal — this audit must never abort a run.
|
||||
_SG_ID="$1" _SG_NAME="$2" _SG_PORT="$3" _SG_EXPECTED="$4" python3 -c "
|
||||
import json, os, sys
|
||||
perms = json.load(sys.stdin) or []
|
||||
port, expected = int(os.environ[\"_SG_PORT\"]), os.environ[\"_SG_EXPECTED\"]
|
||||
extras = []
|
||||
for p in perms:
|
||||
proto, lo, hi = p.get(\"IpProtocol\"), p.get(\"FromPort\"), p.get(\"ToPort\")
|
||||
scope_ok = proto == \"tcp\" and lo == port and hi == port
|
||||
sources = (
|
||||
[r.get(\"CidrIp\", \"?\") for r in p.get(\"IpRanges\", [])]
|
||||
+ [r.get(\"CidrIpv6\", \"?\") for r in p.get(\"Ipv6Ranges\", [])]
|
||||
+ [r.get(\"GroupId\", \"?\") for r in p.get(\"UserIdGroupPairs\", [])]
|
||||
+ [r.get(\"PrefixListId\", \"?\") for r in p.get(\"PrefixListIds\", [])]
|
||||
)
|
||||
extras += [(proto, lo, hi, s) for s in sources if not (scope_ok and s == expected)]
|
||||
if extras:
|
||||
name, gid = os.environ[\"_SG_NAME\"], os.environ[\"_SG_ID\"]
|
||||
print(f\" WARN — security group {name} ({gid}) has ingress beyond the intended rule\", file=sys.stderr)
|
||||
print(f\" (tcp {port} from {expected}) — review it; remove anything you did not add deliberately:\", file=sys.stderr)
|
||||
for proto, lo, hi, src in extras:
|
||||
scope = \"all traffic\" if proto == \"-1\" else (f\"{proto} {lo}\" if lo == hi else f\"{proto} {lo}-{hi}\")
|
||||
print(f\" {scope} from {src}\", file=sys.stderr)
|
||||
" <<<"${perms}" || echo " WARN — could not audit ingress rules on $2 ($1)." >&2
|
||||
}
|
||||
|
||||
secret_arn() {
|
||||
aws secretsmanager describe-secret --secret-id "$1" \
|
||||
--query ARN --output text 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Existence check that fails closed: 0 = exists, 1 = definitively absent
|
||||
# (ResourceNotFoundException), anything else ABORTS the run. Gating on a bare
|
||||
# exit status would let a transient failure (throttle, expired token, network
|
||||
# blip) masquerade as "secret missing" — and the missing-secret branches below
|
||||
# do destructive work (the §3 self-heal resets the DB password), so they must
|
||||
# run only on a definitive not-found.
|
||||
secret_exists() { # <secret-id>
|
||||
local out
|
||||
if out="$(aws secretsmanager describe-secret --secret-id "$1" 2>&1 >/dev/null)"; then
|
||||
return 0
|
||||
elif grep -q 'ResourceNotFoundException' <<<"${out}"; then
|
||||
return 1
|
||||
else
|
||||
echo "ERROR: could not determine whether secret $1 exists (transient AWS error?):" >&2
|
||||
printf '%s\n' "${out}" >&2
|
||||
echo " Refusing to guess — re-run once the call succeeds." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Secret values must never appear on a process argv (argv is world-readable
|
||||
# via /proc and routinely recorded by EDR/auditd), so every aws call that
|
||||
# carries one takes it via --cli-input-json file://<0600 temp file> instead —
|
||||
# explicit flags on the same command line override/merge with the JSON, so
|
||||
# only the secret parameter needs to live in the file. secret_json writes
|
||||
# {"<Key>": "<value>"} to a fresh temp file and returns the path in the named
|
||||
# variable (printf -v, not command substitution — a subshell would lose the
|
||||
# SECRET_TMP_FILES bookkeeping below): the value crosses into python3 via the
|
||||
# environment (never argv) and json.dumps escapes it, so any characters
|
||||
# survive. Callers rm -f the file as soon as the aws call returns; the EXIT
|
||||
# trap sweeps whatever an aborted run leaves.
|
||||
SECRET_TMP_FILES=()
|
||||
cleanup_secret_tmp() { rm -f "${SECRET_TMP_FILES[@]+"${SECRET_TMP_FILES[@]}"}"; }
|
||||
trap cleanup_secret_tmp EXIT
|
||||
secret_json() { # secret_json <outvar> <JsonKey> <value> -> path in <outvar>
|
||||
local file
|
||||
file="$(mktemp)" # mktemp creates 0600
|
||||
chmod 600 "${file}" # belt and braces if TMPDIR overrides umask semantics
|
||||
SECRET_TMP_FILES+=("${file}")
|
||||
_JSON_KEY="$2" _JSON_VALUE="$3" python3 -c \
|
||||
'import json, os; print(json.dumps({os.environ["_JSON_KEY"]: os.environ["_JSON_VALUE"]}))' \
|
||||
> "${file}"
|
||||
printf -v "$1" '%s' "${file}"
|
||||
}
|
||||
|
||||
for required in AWS_REGION ACCOUNT_ID VPC_ID PRIVATE_SUBNETS CORP_CIDR VERSION; do
|
||||
if [[ -z "${!required}" ]]; then
|
||||
echo "ERROR: ${required} is not set." >&2
|
||||
case "${required}" in
|
||||
AWS_REGION) echo " Set it to a region where Bedrock serves the Claude models you need, e.g. export AWS_REGION=us-east-1" >&2 ;;
|
||||
ACCOUNT_ID) echo " Could not resolve it from STS — is the AWS CLI authenticated? (aws sts get-caller-identity)" >&2 ;;
|
||||
VPC_ID) echo " Set it to the VPC from the prerequisites, e.g. export VPC_ID=vpc-..." >&2 ;;
|
||||
PRIVATE_SUBNETS) echo " Set it to two+ private subnet IDs in different AZs, e.g. export PRIVATE_SUBNETS='subnet-a subnet-b'" >&2 ;;
|
||||
CORP_CIDR) echo " Set it to your corporate network CIDR (the ALB's :443 ingress source), e.g. export CORP_CIDR=10.0.0.0/8" >&2 ;;
|
||||
VERSION) echo " Set it to the gateway release version — it tags the image you build and push, e.g. export VERSION=<version>" >&2 ;;
|
||||
esac
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# The walkthrough (and this bundle) is scoped to commercial US regions: the
|
||||
# task role's Bedrock policy (§2) and the gateway's built-in model catalog
|
||||
# both use the us.anthropic.* geo-prefixed cross-region inference profiles,
|
||||
# which only exist in the commercial US regions — an explicit list, not a
|
||||
# `us-*` prefix match, because GovCloud (us-gov-*) and ISO (us-iso-*) regions
|
||||
# share the prefix but live in different AWS partitions where those profiles
|
||||
# and this bundle's arn:aws: ARNs are wrong. Anywhere else the deploy
|
||||
# provisions fine and then every model call fails. Other-region deploys must
|
||||
# pin region-appropriate inference profiles via a models: block in
|
||||
# gateway.yaml (see the config reference:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-config) and adjust the
|
||||
# inference-profile ARN prefix in bedrock-invoke.iam.json below — set
|
||||
# ALLOW_NON_US_REGION=1 once that's done to proceed.
|
||||
case "${AWS_REGION}" in
|
||||
us-east-1|us-east-2|us-west-1|us-west-2) ;;
|
||||
*)
|
||||
if [[ "${ALLOW_NON_US_REGION:-0}" != "1" ]]; then
|
||||
echo "ERROR: AWS_REGION=${AWS_REGION} is not a commercial US region, but this bundle's IAM policy" >&2
|
||||
echo " and model IDs use the US-geo (us.anthropic.*) cross-region inference profiles" >&2
|
||||
echo " (GovCloud/ISO regions are different partitions — the profiles and arn:aws: ARNs" >&2
|
||||
echo " here do not exist there)." >&2
|
||||
echo " Either deploy to us-east-1/us-east-2/us-west-1/us-west-2, or pin region-appropriate" >&2
|
||||
echo " inference profiles in a models: block in gateway.yaml" >&2
|
||||
echo " (https://code.claude.com/docs/en/claude-apps-gateway-config), adjust the" >&2
|
||||
echo " inference-profile ARN in the bedrock-invoke policy, and re-run with" >&2
|
||||
echo " ALLOW_NON_US_REGION=1." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "ERROR: python3 is required (it JSON-escapes secret values for --cli-input-json; the AWS CLI itself ships on Python)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086 # PRIVATE_SUBNETS is intentionally word-split everywhere below
|
||||
set -- ${PRIVATE_SUBNETS}
|
||||
if (( $# < 2 )); then
|
||||
echo "ERROR: PRIVATE_SUBNETS must list at least two subnets in different AZs (the internal ALB requires two)." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Normalize whatever whitespace (spaces, tabs, newlines) separates the list —
|
||||
# `set --` above word-split on IFS, so join those same words with commas
|
||||
# rather than only converting single spaces.
|
||||
SUBNETS_CSV="$(printf '%s,' "$@")"; SUBNETS_CSV="${SUBNETS_CSV%,}"
|
||||
|
||||
log "Account: ${ACCOUNT_ID} Region: ${AWS_REGION} VPC: ${VPC_ID}"
|
||||
|
||||
# ---- 1 Security groups -----------------------------------------------------
|
||||
# Three groups chain the traffic path (walkthrough §1): corp network -> ALB :443,
|
||||
# ALB -> gateway :8080, gateway -> Postgres :5432. Nothing else is reachable.
|
||||
log "Creating security groups (§1)"
|
||||
ALB_SG="$(sg_id "${ALB_SG_NAME}")"
|
||||
if [[ "${ALB_SG}" != "None" ]]; then
|
||||
skip "security group ${ALB_SG_NAME} (${ALB_SG})"
|
||||
else
|
||||
ALB_SG="$(aws ec2 create-security-group --group-name "${ALB_SG_NAME}" \
|
||||
--description "Claude gateway ALB" --vpc-id "${VPC_ID}" \
|
||||
--query GroupId --output text)"
|
||||
fi
|
||||
|
||||
GW_SG="$(sg_id "${GW_SG_NAME}")"
|
||||
if [[ "${GW_SG}" != "None" ]]; then
|
||||
skip "security group ${GW_SG_NAME} (${GW_SG})"
|
||||
else
|
||||
GW_SG="$(aws ec2 create-security-group --group-name "${GW_SG_NAME}" \
|
||||
--description "Claude gateway service" --vpc-id "${VPC_ID}" \
|
||||
--query GroupId --output text)"
|
||||
fi
|
||||
|
||||
DB_SG="$(sg_id "${DB_SG_NAME}")"
|
||||
if [[ "${DB_SG}" != "None" ]]; then
|
||||
skip "security group ${DB_SG_NAME} (${DB_SG})"
|
||||
else
|
||||
DB_SG="$(aws ec2 create-security-group --group-name "${DB_SG_NAME}" \
|
||||
--description "Claude gateway Postgres" --vpc-id "${VPC_ID}" \
|
||||
--query GroupId --output text)"
|
||||
fi
|
||||
|
||||
authorize_ingress --group-id "${ALB_SG}" --protocol tcp --port 443 --cidr "${CORP_CIDR}"
|
||||
authorize_ingress --group-id "${GW_SG}" --protocol tcp --port 8080 --source-group "${ALB_SG}"
|
||||
authorize_ingress --group-id "${DB_SG}" --protocol tcp --port 5432 --source-group "${GW_SG}"
|
||||
|
||||
# Flag any ingress beyond the three rules above (pre-existing groups may carry more).
|
||||
warn_unexpected_ingress "${ALB_SG}" "${ALB_SG_NAME}" 443 "${CORP_CIDR}"
|
||||
warn_unexpected_ingress "${GW_SG}" "${GW_SG_NAME}" 8080 "${ALB_SG}"
|
||||
warn_unexpected_ingress "${DB_SG}" "${DB_SG_NAME}" 5432 "${GW_SG}"
|
||||
|
||||
# ---- 2 IAM roles ------------------------------------------------------------
|
||||
# Task role: the gateway's runtime identity — its ONLY permission is invoking
|
||||
# Claude models on Bedrock (the upstream's `auth: {}` resolves to this role via
|
||||
# the AWS default credential chain). The policy must cover both the cross-region
|
||||
# inference-profile ARNs and the underlying foundation-model ARNs.
|
||||
# Execution role: the ECS agent's identity — pulls the image from ECR and
|
||||
# injects the Secrets Manager values; the gateway never uses it.
|
||||
log "Creating IAM roles ${TASK_ROLE} + ${EXEC_ROLE} (§2)"
|
||||
cat > ecs-trust.iam.json <<'EOF'
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Principal": { "Service": "ecs-tasks.amazonaws.com" },
|
||||
"Action": "sts:AssumeRole"
|
||||
}]
|
||||
}
|
||||
EOF
|
||||
cat > bedrock-invoke.iam.json <<EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
|
||||
"Resource": [
|
||||
"arn:aws:bedrock:${AWS_REGION}:${ACCOUNT_ID}:inference-profile/us.anthropic.*",
|
||||
"arn:aws:bedrock:*::foundation-model/anthropic.*"
|
||||
]
|
||||
}]
|
||||
}
|
||||
EOF
|
||||
# One ARN per secret (never a bare gateway-* wildcard, which would also match
|
||||
# unrelated secrets in a shared account). The trailing -?????? matches exactly
|
||||
# the random 6-character suffix Secrets Manager appends to every secret's ARN
|
||||
# (AWS's documented pattern; a trailing -* would be a plain prefix glob and
|
||||
# also match longer names like ${SECRET_NAME}-prod) — the exact ARNs aren't
|
||||
# knowable here because the role is created before the secrets are.
|
||||
cat > secrets-read.iam.json <<EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Action": "secretsmanager:GetSecretValue",
|
||||
"Resource": [
|
||||
"arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:${JWT_SECRET_NAME}-??????",
|
||||
"arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:${OIDC_SECRET_NAME}-??????",
|
||||
"arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:${SECRET_NAME}-??????"
|
||||
]
|
||||
}]
|
||||
}
|
||||
EOF
|
||||
|
||||
if aws iam get-role --role-name "${TASK_ROLE}" >/dev/null 2>&1; then
|
||||
skip "role ${TASK_ROLE}"
|
||||
else
|
||||
aws iam create-role --role-name "${TASK_ROLE}" \
|
||||
--assume-role-policy-document file://ecs-trust.iam.json >/dev/null
|
||||
fi
|
||||
# put-role-policy is an upsert — safe to re-run (it also picks up region changes).
|
||||
aws iam put-role-policy --role-name "${TASK_ROLE}" \
|
||||
--policy-name bedrock-invoke --policy-document file://bedrock-invoke.iam.json
|
||||
|
||||
if aws iam get-role --role-name "${EXEC_ROLE}" >/dev/null 2>&1; then
|
||||
skip "role ${EXEC_ROLE}"
|
||||
else
|
||||
aws iam create-role --role-name "${EXEC_ROLE}" \
|
||||
--assume-role-policy-document file://ecs-trust.iam.json >/dev/null
|
||||
fi
|
||||
# attach-role-policy is idempotent (re-attaching is a no-op).
|
||||
aws iam attach-role-policy --role-name "${EXEC_ROLE}" \
|
||||
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
|
||||
aws iam put-role-policy --role-name "${EXEC_ROLE}" \
|
||||
--policy-name read-gateway-secrets --policy-document file://secrets-read.iam.json
|
||||
|
||||
echo " NOTE: Bedrock model access is console-only — enable it for the Claude models"
|
||||
echo " you need (Bedrock console -> Model access), and submit the one-time use"
|
||||
echo " case form for the account. Cross-region inference profiles"
|
||||
echo " (us.anthropic.*) need access in EACH region the profile spans."
|
||||
|
||||
# ---- 6 Build & push image to Amazon ECR (config baked in — §6 + §4) ---------
|
||||
log "Ensuring ECR repository and image (§6)"
|
||||
if aws ecr describe-repositories --repository-names "${ECR_REPO}" >/dev/null 2>&1; then
|
||||
skip "ECR repository ${ECR_REPO}"
|
||||
# Integrity-critical settings: converge on re-runs so a pre-existing MUTABLE repo can't slip through.
|
||||
aws ecr put-image-tag-mutability --repository-name "${ECR_REPO}" \
|
||||
--image-tag-mutability IMMUTABLE >/dev/null
|
||||
aws ecr put-image-scanning-configuration --repository-name "${ECR_REPO}" \
|
||||
--image-scanning-configuration scanOnPush=true >/dev/null
|
||||
else
|
||||
# IMMUTABLE tags + scan-on-push: the ECS service pulls whatever this repo
|
||||
# serves under the deployed tag, so a pushed tag must never be silently
|
||||
# re-pointed. For production, also restrict push rights on this repo to your
|
||||
# CI / image-promotion pipeline rather than operator credentials — this
|
||||
# walkthrough pushes directly for simplicity.
|
||||
aws ecr create-repository --repository-name "${ECR_REPO}" \
|
||||
--image-tag-mutability IMMUTABLE \
|
||||
--image-scanning-configuration scanOnPush=true >/dev/null
|
||||
fi
|
||||
|
||||
# The config is baked into the image, so the build is gated the way the GCP
|
||||
# example gates its config-secret publish: gateway.yaml must exist and be fully
|
||||
# filled in (REPLACE_ME checked on non-comment lines so commented examples and
|
||||
# the file's header don't trip the guard). The tag carries a hash of the config
|
||||
# so an edit produces a NEW tag (required by tag immutability) and a re-run
|
||||
# rebuilds automatically.
|
||||
IMAGE=""
|
||||
if [[ ! -f "${GATEWAY_YAML}" ]]; then
|
||||
echo " (skip) ${GATEWAY_YAML} not found — run 'cp gateway.yaml.example gateway.yaml', fill it in, then re-run (§4)."
|
||||
elif grep -vE '^[[:space:]]*#' "${GATEWAY_YAML}" | grep -q 'REPLACE_ME'; then
|
||||
echo " (skip) ${GATEWAY_YAML} still has REPLACE_ME placeholders to fill:"
|
||||
grep -nE 'REPLACE_ME' "${GATEWAY_YAML}" | grep -vE '^[0-9]+:[[:space:]]*#' | sed 's/^/ /'
|
||||
echo " Fill them in, then re-run to build the image (the config is baked in)."
|
||||
else
|
||||
CONFIG_SHA="$(sha_of "${GATEWAY_YAML}" | cut -c1-8)"
|
||||
IMAGE_TAG="${IMAGE_TAG:-${VERSION}-cfg${CONFIG_SHA}}"
|
||||
IMAGE="${REGISTRY}/${ECR_REPO}:${IMAGE_TAG}"
|
||||
|
||||
# Image is the expensive, already-done step: skip the build+push entirely if
|
||||
# the tag already exists in the registry.
|
||||
if aws ecr describe-images --repository-name "${ECR_REPO}" \
|
||||
--image-ids "imageTag=${IMAGE_TAG}" >/dev/null 2>&1; then
|
||||
skip "image ${IMAGE}"
|
||||
else
|
||||
# When the expected checksum is known, verify a PRE-EXISTING binary too:
|
||||
# the [[ ! -f ]] guard below otherwise trusts whatever is on disk, so a
|
||||
# stale binary from an earlier VERSION (or a tampered one) would be baked
|
||||
# into the image silently. On mismatch, set it aside (never delete — the
|
||||
# mismatch may be a typo'd DIST_SHA256, not a bad binary) and fall through
|
||||
# to the fail-closed download path. Without DIST_SHA256 the operator-
|
||||
# provided-binary flow is unchanged — no checksum was declared, so none is
|
||||
# checked.
|
||||
QUARANTINED_SHA=""
|
||||
if [[ -n "${DIST_SHA256}" && -f "${CLAUDE_BINARY}" ]]; then
|
||||
existing_sha="$(sha_of "${CLAUDE_BINARY}")"
|
||||
if [[ "${existing_sha}" != "${DIST_SHA256}" ]]; then
|
||||
log "Existing ${CLAUDE_BINARY} sha256 ${existing_sha} does not match DIST_SHA256 — setting it aside as ${CLAUDE_BINARY}.bad"
|
||||
mv -f "${CLAUDE_BINARY}" "${CLAUDE_BINARY}.bad"
|
||||
QUARANTINED_SHA="${existing_sha}"
|
||||
fi
|
||||
fi
|
||||
if [[ ! -f "${CLAUDE_BINARY}" ]]; then
|
||||
if [[ -n "${DIST_URL}" ]]; then
|
||||
# Fail closed: never download an executable we can't verify.
|
||||
if [[ -z "${DIST_SHA256}" ]]; then
|
||||
echo "ERROR: DIST_SHA256 must be set when DIST_URL is used — refusing to download an unverified binary." >&2
|
||||
echo " Set DIST_SHA256 to the expected sha256 of the binary at DIST_URL, obtained out-of-band:" >&2
|
||||
echo " for standard-release binaries, from the release's GPG-signed manifest.json (verify the" >&2
|
||||
echo " manifest signature first — see code.claude.com/docs/en/setup#binary-integrity-and-code-signing);" >&2
|
||||
echo " otherwise from the channel that published the download link, never from the download server." >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Downloading gateway binary from ${DIST_URL}"
|
||||
# Download to a temp path and only mv into place after the checksum
|
||||
# verifies, so an interrupted download can't leave a partial CLAUDE_BINARY
|
||||
# that the [[ ! -f ]] guard above would skip — and silently push — on re-run.
|
||||
# Refuse plaintext/protocol-downgrade; only follow HTTPS redirects.
|
||||
dl_tmp="${CLAUDE_BINARY}.download"
|
||||
rm -f "${dl_tmp}"
|
||||
curl_https -fL -o "${dl_tmp}" "${DIST_URL}"
|
||||
actual_sha="$(sha_of "${dl_tmp}")"
|
||||
if [[ "${actual_sha}" != "${DIST_SHA256}" ]]; then
|
||||
echo "ERROR: checksum mismatch for ${dl_tmp} (expected ${DIST_SHA256}, got ${actual_sha}) — refusing to build." >&2
|
||||
rm -f "${dl_tmp}"
|
||||
exit 1
|
||||
fi
|
||||
log "Verified binary sha256 ${actual_sha}"
|
||||
chmod +x "${dl_tmp}"
|
||||
mv -f "${dl_tmp}" "${CLAUDE_BINARY}"
|
||||
else
|
||||
echo "ERROR: build binary not found at ${CLAUDE_BINARY} and DIST_URL is not set." >&2
|
||||
if [[ -n "${QUARANTINED_SHA}" ]]; then
|
||||
echo " The binary that WAS there had sha256 ${QUARANTINED_SHA}, which does not match" >&2
|
||||
echo " DIST_SHA256=${DIST_SHA256} — it was preserved as ${CLAUDE_BINARY}.bad." >&2
|
||||
echo " If DIST_SHA256 was a typo, fix it and move the file back:" >&2
|
||||
echo " mv '${CLAUDE_BINARY}.bad' '${CLAUDE_BINARY}'" >&2
|
||||
echo " Otherwise treat that file as untrusted and obtain a verified binary." >&2
|
||||
fi
|
||||
echo " Provide the prebuilt linux-x64 Claude Code release binary at that path" >&2
|
||||
echo " or set DIST_URL to its download URL (see the walkthrough, §6)." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# The RDS CA bundle is baked into the image as the trust anchor for the
|
||||
# connection string's sslmode=verify-full (§3/§5). Fail closed: no bundle,
|
||||
# no build. AWS rotates the bundle, so no checksum is pinned (see the
|
||||
# RDS_CA_BUNDLE_URL comment up top); the sanity check below catches an
|
||||
# error page or truncated download.
|
||||
if [[ ! -f "${RDS_CA_BUNDLE}" ]]; then
|
||||
log "Downloading RDS CA bundle from ${RDS_CA_BUNDLE_URL}"
|
||||
curl_https -fL -o "${RDS_CA_BUNDLE}" "${RDS_CA_BUNDLE_URL}"
|
||||
fi
|
||||
if ! grep -q 'BEGIN CERTIFICATE' "${RDS_CA_BUNDLE}" \
|
||||
|| (( "$(wc -c < "${RDS_CA_BUNDLE}")" < 10000 )); then
|
||||
echo "ERROR: ${RDS_CA_BUNDLE} does not look like the RDS CA bundle (missing PEM blocks or implausibly small) — refusing to build." >&2
|
||||
echo " Delete it and re-run to re-download, or place the bundle from ${RDS_CA_BUNDLE_URL} there yourself." >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Building and pushing ${IMAGE}"
|
||||
aws ecr get-login-password --region "${AWS_REGION}" \
|
||||
| docker login --username AWS --password-stdin "${REGISTRY}"
|
||||
# The task definition below runs linux/amd64 (cpuArchitecture X86_64);
|
||||
# --platform forces it (e.g. when building on an Apple Silicon Mac), and
|
||||
# --provenance=false keeps buildx from wrapping the result in an OCI image
|
||||
# index that some pullers reject. For Fargate on ARM64 (Graviton), build
|
||||
# linux/arm64 with the linux-arm64 binary and set cpuArchitecture to ARM64.
|
||||
docker build --platform=linux/amd64 --provenance=false \
|
||||
-f "${DOCKERFILE}" \
|
||||
--build-arg CLAUDE_BINARY="${CLAUDE_BINARY}" \
|
||||
--build-arg GATEWAY_CONFIG="${GATEWAY_YAML}" \
|
||||
--build-arg RDS_CA_BUNDLE="${RDS_CA_BUNDLE}" \
|
||||
-t "${IMAGE}" .
|
||||
docker push "${IMAGE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 3 RDS for PostgreSQL (private subnets, no public address) --------------
|
||||
log "Creating DB subnet group ${DB_SUBNET_GROUP} (§3)"
|
||||
if aws rds describe-db-subnet-groups --db-subnet-group-name "${DB_SUBNET_GROUP}" >/dev/null 2>&1; then
|
||||
skip "DB subnet group ${DB_SUBNET_GROUP}"
|
||||
else
|
||||
# shellcheck disable=SC2086 # subnet IDs are separate arguments by design
|
||||
aws rds create-db-subnet-group --db-subnet-group-name "${DB_SUBNET_GROUP}" \
|
||||
--db-subnet-group-description "Claude gateway" --subnet-ids ${PRIVATE_SUBNETS} >/dev/null
|
||||
fi
|
||||
|
||||
# Parameter group with rds.force_ssl=1: the server side of TLS enforcement —
|
||||
# the client side is sslmode=verify-full in the connection string (§5). The
|
||||
# family must match the engine major version, so it derives from the same
|
||||
# DB_ENGINE_VERSION that create-db-instance pins below.
|
||||
log "Ensuring DB parameter group ${DB_PARAM_GROUP} (rds.force_ssl=1)"
|
||||
PG_FAMILY="postgres${DB_ENGINE_VERSION%%.*}"
|
||||
if aws rds describe-db-parameter-groups --db-parameter-group-name "${DB_PARAM_GROUP}" >/dev/null 2>&1; then
|
||||
skip "DB parameter group ${DB_PARAM_GROUP}"
|
||||
else
|
||||
aws rds create-db-parameter-group --db-parameter-group-name "${DB_PARAM_GROUP}" \
|
||||
--db-parameter-group-family "${PG_FAMILY}" \
|
||||
--description "Claude gateway - require TLS on every connection" >/dev/null
|
||||
fi
|
||||
# modify-db-parameter-group is an upsert — applied every run so a pre-existing
|
||||
# group converges too. rds.force_ssl is dynamic; no reboot needed.
|
||||
aws rds modify-db-parameter-group --db-parameter-group-name "${DB_PARAM_GROUP}" \
|
||||
--parameters "ParameterName=rds.force_ssl,ParameterValue=1,ApplyMethod=immediate" >/dev/null
|
||||
|
||||
# hex (not base64) keeps the password URL-safe for the connection string below.
|
||||
# The password reaches every aws call via --cli-input-json (never argv — see
|
||||
# the secret_json helper); explicit flags merge with (and would override) the
|
||||
# JSON, so only the password lives in the temp file.
|
||||
log "Creating RDS instance ${DB_INSTANCE} (private subnets, --no-publicly-accessible)"
|
||||
DB_PASSWORD=""
|
||||
DB_POSTURE="$(aws rds describe-db-instances --db-instance-identifier "${DB_INSTANCE}" \
|
||||
--query 'DBInstances[0].[PubliclyAccessible,StorageEncrypted]' --output text 2>/dev/null || true)"
|
||||
if [[ -n "${DB_POSTURE}" ]]; then
|
||||
# Name-based reuse: a pre-existing instance may not carry the posture this
|
||||
# script would have created it with. Non-fatal (the operator may be migrating
|
||||
# an existing DB on purpose), but drift from the guide's baseline must be seen.
|
||||
read -r DB_PUBLIC DB_ENCRYPTED <<<"${DB_POSTURE}"
|
||||
if [[ "${DB_PUBLIC}" == "True" ]]; then
|
||||
echo " WARN — RDS instance ${DB_INSTANCE} is PubliclyAccessible; this script would have" >&2
|
||||
echo " created it with --no-publicly-accessible. Fix: aws rds modify-db-instance" >&2
|
||||
echo " --db-instance-identifier ${DB_INSTANCE} --no-publicly-accessible --apply-immediately" >&2
|
||||
fi
|
||||
if [[ "${DB_ENCRYPTED}" == "False" ]]; then
|
||||
echo " WARN — RDS instance ${DB_INSTANCE} has StorageEncrypted=false; this script would" >&2
|
||||
echo " have created it with --storage-encrypted (encryption cannot be enabled in" >&2
|
||||
echo " place — restore an encrypted snapshot copy to migrate)." >&2
|
||||
fi
|
||||
if secret_exists "${SECRET_NAME}"; then
|
||||
skip "instance ${DB_INSTANCE} (password unchanged; secret not rewritten)"
|
||||
else
|
||||
# Self-heal: a previous run died after creating the instance but before
|
||||
# writing the connection-string secret, losing the only copy of the
|
||||
# password. The secret is the password's only consumer, so resetting it is
|
||||
# safe and keeps re-runs able to recover from any partial state.
|
||||
# secret_exists (not a bare exit-status check) gates this: only a
|
||||
# definitive ResourceNotFoundException may trigger a password reset.
|
||||
# ORDERING INVARIANT: the secret write (§5 below) is the heal's commit
|
||||
# point — everything that can fail must happen BEFORE it, so a crash at
|
||||
# any point leaves the secret still missing and the next run simply
|
||||
# repeats the heal. Writing the secret first would invert that: a crash
|
||||
# between secret write and modify-db-instance would leave an existing
|
||||
# secret whose password the DB never received, and every later run would
|
||||
# skip the heal while the gateway can't connect.
|
||||
# NOTE: the parameter group is attached on create only — an instance that
|
||||
# predates it keeps its current group (attach via modify-db-instance
|
||||
# --db-parameter-group-name yourself if you want force_ssl retrofitted).
|
||||
log "Instance ${DB_INSTANCE} exists but secret ${SECRET_NAME} is missing — resetting password"
|
||||
DB_PASSWORD="$(openssl rand -hex 24)"
|
||||
pw_json=""; secret_json pw_json MasterUserPassword "${DB_PASSWORD}"
|
||||
aws rds modify-db-instance --db-instance-identifier "${DB_INSTANCE}" \
|
||||
--cli-input-json "file://${pw_json}" --apply-immediately >/dev/null
|
||||
rm -f "${pw_json}"
|
||||
fi
|
||||
else
|
||||
DB_PASSWORD="$(openssl rand -hex 24)"
|
||||
pw_json=""; secret_json pw_json MasterUserPassword "${DB_PASSWORD}"
|
||||
aws rds create-db-instance --db-instance-identifier "${DB_INSTANCE}" \
|
||||
--engine postgres --engine-version "${DB_ENGINE_VERSION}" \
|
||||
--db-instance-class "${DB_CLASS}" \
|
||||
--allocated-storage "${DB_STORAGE_GB}" --db-name "${DB_NAME}" \
|
||||
--master-username "${DB_USER}" --cli-input-json "file://${pw_json}" \
|
||||
--db-subnet-group-name "${DB_SUBNET_GROUP}" \
|
||||
--db-parameter-group-name "${DB_PARAM_GROUP}" \
|
||||
--vpc-security-group-ids "${DB_SG}" \
|
||||
--no-publicly-accessible \
|
||||
--storage-encrypted >/dev/null
|
||||
rm -f "${pw_json}"
|
||||
fi
|
||||
|
||||
log "Waiting for ${DB_INSTANCE} to become available (first creation takes ~10 min)"
|
||||
aws rds wait db-instance-available --db-instance-identifier "${DB_INSTANCE}"
|
||||
DB_HOST="$(aws rds describe-db-instances --db-instance-identifier "${DB_INSTANCE}" \
|
||||
--query 'DBInstances[0].Endpoint.Address' --output text)"
|
||||
|
||||
# ---- 5 Connection string + JWT secret -> Secrets Manager --------------------
|
||||
# No per-secret IAM grants are needed: the execution role's read-gateway-secrets
|
||||
# policy (§2) names each of the three secrets by its ARN prefix.
|
||||
# Secret values go to aws via --cli-input-json temp files, never argv.
|
||||
if [[ -n "${DB_PASSWORD}" ]]; then
|
||||
# RDS private endpoint (guide §3); the gateway connects directly over the
|
||||
# VPC — the DB security group only admits ${GW_SG_NAME}.
|
||||
# sslmode=verify-full: the gateway's driver honors sslmode from the URL and
|
||||
# verifies the RDS certificate chain AND hostname against the CA bundle the
|
||||
# image trusts via NODE_EXTRA_CA_CERTS (see the Dockerfile). Do NOT add a
|
||||
# libpq-style `sslrootcert=` query param — the driver doesn't read it and
|
||||
# forwards it to Postgres as a startup parameter, which the server rejects.
|
||||
CONN="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/${DB_NAME}?sslmode=verify-full"
|
||||
log "Storing connection string in Secrets Manager secret ${SECRET_NAME} (§5)"
|
||||
conn_json=""; secret_json conn_json SecretString "${CONN}"
|
||||
if secret_exists "${SECRET_NAME}"; then
|
||||
aws secretsmanager put-secret-value --secret-id "${SECRET_NAME}" \
|
||||
--cli-input-json "file://${conn_json}" >/dev/null
|
||||
else
|
||||
aws secretsmanager create-secret --name "${SECRET_NAME}" \
|
||||
--cli-input-json "file://${conn_json}" >/dev/null
|
||||
fi
|
||||
rm -f "${conn_json}"
|
||||
else
|
||||
log "Skipping postgres-url secret write (instance already existed, password not available this run)"
|
||||
fi
|
||||
|
||||
# JWT signing secret — generated once (re-runs do NOT rotate it).
|
||||
log "Ensuring JWT signing secret ${JWT_SECRET_NAME} (§5)"
|
||||
if secret_exists "${JWT_SECRET_NAME}"; then
|
||||
skip "secret ${JWT_SECRET_NAME}"
|
||||
else
|
||||
jwt_json=""; secret_json jwt_json SecretString "$(openssl rand -base64 32)"
|
||||
aws secretsmanager create-secret --name "${JWT_SECRET_NAME}" \
|
||||
--cli-input-json "file://${jwt_json}" >/dev/null
|
||||
rm -f "${jwt_json}"
|
||||
fi
|
||||
|
||||
# OIDC client secret — operator-created (the script can't generate it; it comes
|
||||
# from the Okta OIDC web application). Checked here so the deploy step below can
|
||||
# gate on it with a clear message instead of a raw ECS secret-injection failure.
|
||||
OIDC_ARN="$(secret_arn "${OIDC_SECRET_NAME}")"
|
||||
|
||||
# ---- 7 ECS Fargate service + internal ALB ----------------------------------
|
||||
# Self-gating: deploy only once its inputs exist (image pushed — i.e.
|
||||
# gateway.yaml was filled in — plus the operator-provided OIDC client secret
|
||||
# and the ACM certificate for the internal hostname). On a first run these are
|
||||
# usually missing and it cleanly skips.
|
||||
ALB_DNS=""
|
||||
missing=""
|
||||
[[ -n "${IMAGE}" ]] || missing="${missing} image(fill ${GATEWAY_YAML})"
|
||||
[[ -n "${OIDC_ARN}" ]] || missing="${missing} ${OIDC_SECRET_NAME}"
|
||||
[[ -n "${ACM_CERT_ARN}" ]] || missing="${missing} ACM_CERT_ARN"
|
||||
SECRET_ARN="$(secret_arn "${SECRET_NAME}")"
|
||||
JWT_ARN="$(secret_arn "${JWT_SECRET_NAME}")"
|
||||
[[ -n "${SECRET_ARN}" ]] || missing="${missing} ${SECRET_NAME}"
|
||||
[[ -n "${JWT_ARN}" ]] || missing="${missing} ${JWT_SECRET_NAME}"
|
||||
|
||||
if [[ "${DEPLOY}" != "1" ]]; then
|
||||
log "Skipping ECS/ALB deploy (DEPLOY=${DEPLOY}) (§7)"
|
||||
elif [[ -n "${missing// }" ]]; then
|
||||
log "Skipping ECS/ALB deploy — missing input(s):${missing} (§7)"
|
||||
echo " Fill ${GATEWAY_YAML} and re-run to build the image; create ${OIDC_SECRET_NAME}"
|
||||
echo " from the Okta client secret; set ACM_CERT_ARN to the certificate for your"
|
||||
echo " internal gateway hostname. Then re-run to deploy."
|
||||
else
|
||||
log "Creating ECS cluster ${CLUSTER} and log group ${LOG_GROUP} (§7)"
|
||||
if [[ "$(aws ecs describe-clusters --clusters "${CLUSTER}" \
|
||||
--query 'clusters[0].status' --output text 2>/dev/null)" == "ACTIVE" ]]; then
|
||||
skip "cluster ${CLUSTER}"
|
||||
else
|
||||
aws ecs create-cluster --cluster-name "${CLUSTER}" >/dev/null
|
||||
fi
|
||||
# The gateway's stderr carries both its audit events and operational logs.
|
||||
if aws logs describe-log-groups --log-group-name-prefix "${LOG_GROUP}" \
|
||||
--query 'logGroups[?logGroupName==`'"${LOG_GROUP}"'`]' --output text 2>/dev/null | grep -q .; then
|
||||
skip "log group ${LOG_GROUP}"
|
||||
else
|
||||
aws logs create-log-group --log-group-name "${LOG_GROUP}"
|
||||
fi
|
||||
# Retention is a separate API (create-log-group has no retention flag) and an
|
||||
# upsert — applied every run so pre-existing groups converge too. Without it
|
||||
# the group keeps logs forever and cost grows unbounded.
|
||||
aws logs put-retention-policy --log-group-name "${LOG_GROUP}" \
|
||||
--retention-in-days "${LOG_RETENTION_DAYS}"
|
||||
|
||||
# Task definition: the task role carries the Bedrock permission; the
|
||||
# execution role injects the secrets. Registering is an append (a new
|
||||
# revision) — the service below always points at the latest.
|
||||
log "Registering task definition ${TASK_FAMILY}"
|
||||
taskdef_tmp="$(mktemp)"
|
||||
cat > "${taskdef_tmp}" <<EOF
|
||||
{
|
||||
"family": "${TASK_FAMILY}",
|
||||
"networkMode": "awsvpc",
|
||||
"requiresCompatibilities": ["FARGATE"],
|
||||
"cpu": "${TASK_CPU}",
|
||||
"memory": "${TASK_MEMORY}",
|
||||
"runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" },
|
||||
"executionRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/${EXEC_ROLE}",
|
||||
"taskRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/${TASK_ROLE}",
|
||||
"containerDefinitions": [
|
||||
{
|
||||
"name": "gateway",
|
||||
"image": "${IMAGE}",
|
||||
"portMappings": [{ "containerPort": 8080 }],
|
||||
"secrets": [
|
||||
{ "name": "GATEWAY_JWT_SECRET", "valueFrom": "${JWT_ARN}" },
|
||||
{ "name": "OIDC_CLIENT_SECRET", "valueFrom": "${OIDC_ARN}" },
|
||||
{ "name": "GATEWAY_POSTGRES_URL", "valueFrom": "${SECRET_ARN}" }
|
||||
],
|
||||
"logConfiguration": {
|
||||
"logDriver": "awslogs",
|
||||
"options": {
|
||||
"awslogs-group": "${LOG_GROUP}",
|
||||
"awslogs-region": "${AWS_REGION}",
|
||||
"awslogs-stream-prefix": "gateway"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
aws ecs register-task-definition --cli-input-json "file://${taskdef_tmp}" >/dev/null
|
||||
rm -f "${taskdef_tmp}"
|
||||
|
||||
# Internal ALB. --ip-address-type ipv4: an internal dual-stack ALB publishes
|
||||
# public-range AAAA records, which the CLI's /login private-network check
|
||||
# rejects.
|
||||
log "Creating internal ALB ${ALB_NAME} + target group + HTTPS listener"
|
||||
read -r ALB_ARN ALB_SCHEME ALB_VPC ALB_IP_TYPE <<<"$(aws elbv2 describe-load-balancers --names "${ALB_NAME}" \
|
||||
--query 'LoadBalancers[0].[LoadBalancerArn,Scheme,VpcId,IpAddressType]' --output text 2>/dev/null || true)"
|
||||
if [[ -n "${ALB_ARN}" && "${ALB_ARN}" != "None" ]]; then
|
||||
# Reuse is by name, and scheme/VPC are immutable on an ALB — so posture is
|
||||
# asserted, fail-closed: attaching the gateway to an internet-facing or
|
||||
# wrong-VPC load balancer would change the exposure model, not just drift.
|
||||
if [[ "${ALB_SCHEME}" != "internal" || "${ALB_VPC}" != "${VPC_ID}" ]]; then
|
||||
echo "ERROR: load balancer ${ALB_NAME} exists but is not the internal ALB this script expects:" >&2
|
||||
echo " scheme=${ALB_SCHEME} (need internal), vpc=${ALB_VPC} (need ${VPC_ID})." >&2
|
||||
echo " Refusing to deploy the gateway behind it. Delete that load balancer, or set" >&2
|
||||
echo " ALB_NAME to an unused name, then re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
skip "load balancer ${ALB_NAME} (internal, ${ALB_VPC})"
|
||||
# ip-address-type IS mutable (unlike scheme/VPC) — converge a reused
|
||||
# dualstack ALB back to ipv4, matching the Terraform sibling: dual-stack
|
||||
# publishes public-range AAAA records that /login rejects (see above).
|
||||
if [[ "${ALB_IP_TYPE}" != "ipv4" ]]; then
|
||||
aws elbv2 set-ip-address-type --load-balancer-arn "${ALB_ARN}" \
|
||||
--ip-address-type ipv4 >/dev/null
|
||||
fi
|
||||
else
|
||||
# shellcheck disable=SC2086
|
||||
ALB_ARN="$(aws elbv2 create-load-balancer --name "${ALB_NAME}" \
|
||||
--scheme internal --type application --ip-address-type ipv4 \
|
||||
--subnets ${PRIVATE_SUBNETS} --security-groups "${ALB_SG}" \
|
||||
--query 'LoadBalancers[0].LoadBalancerArn' --output text)"
|
||||
fi
|
||||
|
||||
# The ALB closes a connection after 60 seconds with no data by default, which
|
||||
# cuts off streams during quiet periods (long prompt processing before the
|
||||
# first token, extended thinking). Attribute setting is idempotent.
|
||||
aws elbv2 modify-load-balancer-attributes --load-balancer-arn "${ALB_ARN}" \
|
||||
--attributes Key=idle_timeout.timeout_seconds,Value=3600 >/dev/null
|
||||
|
||||
read -r TG_ARN TG_VPC <<<"$(aws elbv2 describe-target-groups --names "${TG_NAME}" \
|
||||
--query 'TargetGroups[0].[TargetGroupArn,VpcId]' --output text 2>/dev/null || true)"
|
||||
if [[ -n "${TG_ARN}" && "${TG_ARN}" != "None" ]]; then
|
||||
skip "target group ${TG_NAME}"
|
||||
# VPC is immutable on a target group; a wrong-VPC one can't reach the tasks.
|
||||
if [[ "${TG_VPC}" != "${VPC_ID}" ]]; then
|
||||
echo " WARN — target group ${TG_NAME} is in ${TG_VPC}, not ${VPC_ID}; the service's tasks" >&2
|
||||
echo " will not become healthy behind it. Delete it or set TG_NAME to an unused" >&2
|
||||
echo " name, then re-run." >&2
|
||||
fi
|
||||
else
|
||||
# /readyz verifies the store is reachable, so a task that can't reach
|
||||
# Postgres never enters rotation (the gateway also serves liveness-only
|
||||
# /healthz — see the deploy guide's outage-behavior tradeoff).
|
||||
TG_ARN="$(aws elbv2 create-target-group --name "${TG_NAME}" \
|
||||
--protocol HTTP --port 8080 --vpc-id "${VPC_ID}" --target-type ip \
|
||||
--health-check-path /readyz \
|
||||
--query 'TargetGroups[0].TargetGroupArn' --output text)"
|
||||
fi
|
||||
|
||||
# Select the HTTPS:443 listener specifically — a reused ALB may carry other
|
||||
# listeners (say HTTP:80); those stay untouched, and the 443 listener is
|
||||
# still created when it's the one that's missing.
|
||||
# shellcheck disable=SC2016 # backticks are JMESPath literals, not expansion
|
||||
LISTENER_ARN="$(aws elbv2 describe-listeners --load-balancer-arn "${ALB_ARN}" \
|
||||
--query 'Listeners[?Port==`443`]|[0].ListenerArn' --output text 2>/dev/null || true)"
|
||||
if [[ -n "${LISTENER_ARN}" && "${LISTENER_ARN}" != "None" ]]; then
|
||||
skip "HTTPS:443 listener on ${ALB_NAME}"
|
||||
# Converge everything this script owns on pre-existing listeners
|
||||
# (modify-listener is an upsert): the TLS policy (so re-runs pick up an
|
||||
# ALB_SSL_POLICY change, and listeners created before this script pinned
|
||||
# one lose the legacy default), the certificate (so a changed ACM_CERT_ARN
|
||||
# — e.g. a renewal under a new ARN — is not silently ignored), and the
|
||||
# default action (so the listener always forwards to this target group).
|
||||
aws elbv2 modify-listener --listener-arn "${LISTENER_ARN}" \
|
||||
--ssl-policy "${ALB_SSL_POLICY}" \
|
||||
--certificates "CertificateArn=${ACM_CERT_ARN}" \
|
||||
--default-actions "Type=forward,TargetGroupArn=${TG_ARN}" >/dev/null
|
||||
else
|
||||
aws elbv2 create-listener --load-balancer-arn "${ALB_ARN}" \
|
||||
--protocol HTTPS --port 443 \
|
||||
--ssl-policy "${ALB_SSL_POLICY}" \
|
||||
--certificates "CertificateArn=${ACM_CERT_ARN}" \
|
||||
--default-actions "Type=forward,TargetGroupArn=${TG_ARN}" >/dev/null
|
||||
fi
|
||||
|
||||
# Service: created once, then rolled forward — a re-run points it at the
|
||||
# latest task-definition revision (which carries the current image tag, and
|
||||
# therefore the current gateway.yaml) and forces a new deployment.
|
||||
log "Creating/updating ECS service ${SERVICE} (Fargate, private subnets, no public IP)"
|
||||
svc_status="$(aws ecs describe-services --cluster "${CLUSTER}" --services "${SERVICE}" \
|
||||
--query 'services[0].status' --output text 2>/dev/null || true)"
|
||||
if [[ "${svc_status}" == "ACTIVE" ]]; then
|
||||
aws ecs update-service --cluster "${CLUSTER}" --service "${SERVICE}" \
|
||||
--task-definition "${TASK_FAMILY}" --desired-count "${DESIRED_COUNT}" \
|
||||
--deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true}" \
|
||||
--health-check-grace-period-seconds 60 \
|
||||
--force-new-deployment >/dev/null
|
||||
echo " service updated to the latest task-definition revision."
|
||||
else
|
||||
# All egress (Bedrock, the IdP, Secrets Manager, ECR, CloudWatch Logs) goes
|
||||
# through the NAT gateway — assignPublicIp stays DISABLED.
|
||||
# The deployment circuit breaker stops a rollout whose tasks keep failing
|
||||
# (bad image, unbootable config) and rolls back to the last steady state
|
||||
# instead of relaunching failing tasks forever. The health-check grace
|
||||
# period gives a cold task (image pull + store connect + first /readyz)
|
||||
# time before ECS counts it unhealthy — without it the circuit breaker can
|
||||
# declare the very first rollout failed (matches terraform/'s
|
||||
# health_check_grace_period_seconds).
|
||||
aws ecs create-service --cluster "${CLUSTER}" --service-name "${SERVICE}" \
|
||||
--task-definition "${TASK_FAMILY}" --desired-count "${DESIRED_COUNT}" \
|
||||
--launch-type FARGATE \
|
||||
--deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true}" \
|
||||
--health-check-grace-period-seconds 60 \
|
||||
--network-configuration "awsvpcConfiguration={subnets=[${SUBNETS_CSV}],securityGroups=[${GW_SG}],assignPublicIp=DISABLED}" \
|
||||
--load-balancers "targetGroupArn=${TG_ARN},containerName=gateway,containerPort=8080" >/dev/null
|
||||
fi
|
||||
|
||||
ALB_DNS="$(aws elbv2 describe-load-balancers --load-balancer-arns "${ALB_ARN}" \
|
||||
--query 'LoadBalancers[0].DNSName' --output text)"
|
||||
log "Internal ALB DNS: ${ALB_DNS}"
|
||||
|
||||
# Post-deploy smoke check: the ALB is internal (unreachable from this
|
||||
# machine), but target health is visible through the API — poll until the
|
||||
# /readyz health check passes. Non-fatal; a cold task needs a minute or two
|
||||
# (image pull + store connect).
|
||||
log "Smoke check: polling target health on ${TG_NAME} (health check: GET /readyz)"
|
||||
tg_state="unknown"
|
||||
for _ in $(seq 1 24); do
|
||||
tg_state="$(aws elbv2 describe-target-health --target-group-arn "${TG_ARN}" \
|
||||
--query 'TargetHealthDescriptions[0].TargetHealth.State' --output text 2>/dev/null || true)"
|
||||
[[ "${tg_state}" == "healthy" ]] && break
|
||||
sleep 10
|
||||
done
|
||||
if [[ "${tg_state}" == "healthy" ]]; then
|
||||
echo " OK — a gateway task is healthy behind the ALB (store reachable)."
|
||||
else
|
||||
echo " WARN — last target state: ${tg_state:-none}; the task may still be starting."
|
||||
echo " Check the service events and the gateway's logs:"
|
||||
echo " aws ecs describe-services --cluster ${CLUSTER} --services ${SERVICE} --query 'services[0].events[:5]'"
|
||||
echo " aws logs tail ${LOG_GROUP} --since 10m"
|
||||
fi
|
||||
|
||||
# public_url is baked into the image, so verify the operator's chosen
|
||||
# hostname is in place (the redirect URI and discovery doc derive from it).
|
||||
CFG_PUBLIC_URL="$(grep -E '^[[:space:]]*public_url:' "${GATEWAY_YAML}" 2>/dev/null \
|
||||
| head -1 \
|
||||
| sed -E 's/^[[:space:]]*public_url:[[:space:]]*//; s/[[:space:]]+#.*$//; s/[[:space:]]*$//' \
|
||||
|| true)"
|
||||
CFG_PUBLIC_URL="${CFG_PUBLIC_URL#[\'\"]}"; CFG_PUBLIC_URL="${CFG_PUBLIC_URL%[\'\"]}"
|
||||
CFG_PUBLIC_URL="${CFG_PUBLIC_URL%/}"
|
||||
echo " 1. In your Route 53 private hosted zone, alias the host of"
|
||||
echo " ${CFG_PUBLIC_URL:-<public_url>} to the ALB: ${ALB_DNS}"
|
||||
echo " (the ALB's own *.elb.amazonaws.com name can't carry your ACM certificate)."
|
||||
echo " 2. Register this redirect URI on the Okta OIDC web app: ${CFG_PUBLIC_URL:-<public_url>}/oauth/callback"
|
||||
echo " 3. Verify from inside your corporate network:"
|
||||
echo " curl -s ${CFG_PUBLIC_URL:-<public_url>}/.well-known/oauth-authorization-server"
|
||||
fi
|
||||
|
||||
# ---- summary ----------------------------------------------------------------
|
||||
cat <<EOF
|
||||
|
||||
==> Done.
|
||||
|
||||
Security groups ${ALB_SG_NAME}=${ALB_SG} ${GW_SG_NAME}=${GW_SG} ${DB_SG_NAME}=${DB_SG}
|
||||
IAM roles ${TASK_ROLE} (bedrock-invoke), ${EXEC_ROLE} (pull + secrets)
|
||||
Image ${IMAGE:-(not built yet — fill ${GATEWAY_YAML})}
|
||||
RDS instance ${DB_INSTANCE} -> ${DB_HOST}
|
||||
Database / user ${DB_NAME} / ${DB_USER}
|
||||
Secrets ${SECRET_NAME}, ${JWT_SECRET_NAME}, ${OIDC_SECRET_NAME}$( [[ -n "${OIDC_ARN}" ]] || printf ' (MISSING — create it)' )
|
||||
ECS service ${CLUSTER}/${SERVICE} behind ${ALB_DNS:-(not deployed yet)}
|
||||
|
||||
Next steps (see https://code.claude.com/docs/en/claude-apps-gateway-on-aws):
|
||||
- Create the one operator-provided secret (from the Okta OIDC web app). Put the
|
||||
client secret in a 0600 file first — passing it as a literal argument would
|
||||
leave it readable in the process table and in audit/EDR logs:
|
||||
aws secretsmanager create-secret --name ${OIDC_SECRET_NAME} \\
|
||||
--secret-string file:///path/to/okta-client-secret.txt
|
||||
- Fill in the REPLACE_ME values in ${GATEWAY_YAML}, then re-run: setup.sh builds the
|
||||
image (config baked in) and deploys once the secret and ACM_CERT_ARN exist.
|
||||
- Enable Bedrock model access in the console for the Claude models you need (per
|
||||
region the us.anthropic.* profiles span) and submit the one-time use case form.
|
||||
- Alias your internal hostname (gateway.yaml public_url) to the ALB in a Route 53
|
||||
private hosted zone, and register <public_url>/oauth/callback on the Okta app.
|
||||
- The gateway runs its own schema migrations at boot, so ${DB_USER} needs CREATE TABLE.
|
||||
EOF
|
||||
@@ -0,0 +1,19 @@
|
||||
# Never commit state (contains secrets) or local var files
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
.terraform/
|
||||
terraform.tfvars
|
||||
*.auto.tfvars
|
||||
crash.log
|
||||
|
||||
# The lock file holds no secrets. It's ignored here so consumers who copy this
|
||||
# example into their own repo generate (and commit) their own platform-complete
|
||||
# lock at first init — committing one from this repo would carry only one
|
||||
# platform's provider hashes. In your copy, drop this line and commit the lock
|
||||
# produced by:
|
||||
# terraform providers lock -platform=linux_amd64 -platform=linux_arm64 \
|
||||
# -platform=darwin_amd64 -platform=darwin_arm64 -platform=windows_amd64
|
||||
# versions.tf pins by range only, so without a committed lock the registry
|
||||
# serves the newest in-range build; a platform-complete lock gives hash
|
||||
# continuity across machines/CI and makes provider upgrades reviewable diffs.
|
||||
.terraform.lock.hcl
|
||||
@@ -0,0 +1,182 @@
|
||||
# Claude apps gateway — Terraform (ECS Fargate)
|
||||
|
||||
Terraform equivalent of `../setup.sh`. Lets end-users provision and manage
|
||||
the gateway with `terraform apply`. Covers the same scope ([walkthrough](https://code.claude.com/docs/en/claude-apps-gateway-on-aws) §1–7,
|
||||
ECS track): security groups → task + execution IAM roles → ECR repository →
|
||||
private-subnet RDS for PostgreSQL → Secrets Manager secrets → ECS Fargate
|
||||
service behind an internal ALB. The VPC and private subnets are walkthrough
|
||||
prerequisites, passed in as variables — unlike the GCP example, no network is
|
||||
created here.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `versions.tf` | Provider pins (aws, random) |
|
||||
| `variables.tf` | All inputs (defaults match `setup.sh`'s) |
|
||||
| `main.tf` | Resources |
|
||||
| `outputs.tf` | ALB DNS name + zone ID, image, roles, DB endpoint |
|
||||
| `terraform.tfvars.example` | Copy to `terraform.tfvars` and edit |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **`../gateway.yaml` created and FULLY filled in** — copy the template first:
|
||||
`cp ../gateway.yaml.example ../gateway.yaml`, then replace every `REPLACE_ME`
|
||||
(Terraform reads this file and enforces no `REPLACE_ME` via a precondition).
|
||||
Unlike the GCP example there is no placeholder-first-pass: the config is
|
||||
**baked into the image**, and `public_url` is your own internal hostname,
|
||||
which you choose up front (you already hold its ACM certificate).
|
||||
`gateway.yaml` is gitignored; the committed template is `gateway.yaml.example`.
|
||||
2. The **prebuilt linux-x64 `claude` binary at `../claude`** — the Claude Code
|
||||
release binary, which includes the `gateway` subcommand (see the
|
||||
[walkthrough](https://code.claude.com/docs/en/claude-apps-gateway-on-aws)).
|
||||
See `../setup.sh`'s `DIST_URL`/`DIST_SHA256` download path for a
|
||||
checksum-verified fetch.
|
||||
3. A **VPC with two+ private subnets** in different AZs and NAT egress, an **ACM
|
||||
certificate** for your internal gateway hostname, and **Bedrock model access**
|
||||
enabled in the console (cross-region `us.anthropic.*` profiles need it in each
|
||||
region the profile spans), with the one-time use case form submitted.
|
||||
4. A **remote backend** for shared use (see below). State holds secrets — never commit it.
|
||||
|
||||
## Deploy
|
||||
|
||||
Terraform creates the ECR repository but does **not** build/push the image, so
|
||||
the apply is two passes: a targeted apply to create the repo, then build/push,
|
||||
then the full apply.
|
||||
|
||||
```bash
|
||||
cp terraform.tfvars.example terraform.tfvars # edit it
|
||||
terraform init
|
||||
|
||||
# Pin providers in your copy (once, then commit .terraform.lock.hcl and drop
|
||||
# its .gitignore line): versions.tf pins by range only, so without a committed
|
||||
# lock the registry serves the newest in-range build — a platform-complete
|
||||
# lock gives hash continuity across machines/CI and makes provider upgrades
|
||||
# reviewable diffs.
|
||||
terraform providers lock -platform=linux_amd64 -platform=linux_arm64 \
|
||||
-platform=darwin_amd64 -platform=darwin_arm64 -platform=windows_amd64
|
||||
|
||||
# 1. Create just the ECR repository (the -target warning is expected):
|
||||
terraform apply -target=aws_ecr_repository.repo
|
||||
|
||||
# 2. Build and push the image (gateway.yaml and the RDS CA bundle are baked in;
|
||||
# the COPY sources are context-relative — the build context `..` is aws/, so
|
||||
# `claude`, `gateway.yaml`, and `rds-global-bundle.pem`).
|
||||
# The CA bundle is the trust anchor for the connection string's
|
||||
# sslmode=verify-full (AWS rotates it; download it when absent — don't commit it):
|
||||
curl -fL --proto '=https' -o ../rds-global-bundle.pem \
|
||||
https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
|
||||
aws ecr get-login-password --region us-east-1 \
|
||||
| docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com
|
||||
docker build --platform=linux/amd64 --provenance=false \
|
||||
-f ../Dockerfile --build-arg CLAUDE_BINARY=claude --build-arg GATEWAY_CONFIG=gateway.yaml \
|
||||
-t <account-id>.dkr.ecr.us-east-1.amazonaws.com/claude-gateway:<version> ..
|
||||
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/claude-gateway:<version>
|
||||
|
||||
# 3. Full apply:
|
||||
terraform apply
|
||||
```
|
||||
|
||||
Set in `terraform.tfvars`:
|
||||
|
||||
- `region`, `vpc_id`, `private_subnet_ids`, `corporate_cidr`
|
||||
- `acm_certificate_arn` — the certificate for your internal gateway hostname
|
||||
(`gateway.yaml`'s `public_url` host), served by the ALB's HTTPS listener
|
||||
- `image_tag` (after building/pushing — step 2 above). The repo enforces
|
||||
**immutable tags**, so a `gateway.yaml` edit means a rebuild under a **new**
|
||||
tag and an `image_tag` bump (`../setup.sh` automates this by tagging
|
||||
`<version>-cfg<sha8-of-gateway.yaml>`)
|
||||
- **`oidc_client_secret`** — required (the ECS tasks inject `latest` of this
|
||||
secret at start; with no version they fail with
|
||||
`ResourceInitializationError`). Terraform creates the secret + version from it.
|
||||
|
||||
## Tear down
|
||||
|
||||
Tear down a trial with `terraform destroy`: set `deletion_protection = false`,
|
||||
run `terraform apply` to record that on RDS and the ALB (and to flip RDS to
|
||||
`skip_final_snapshot` — the provider checks the value in **state**, not config,
|
||||
so destroy would still refuse otherwise), then `terraform destroy`.
|
||||
|
||||
The same switch drives the Secrets Manager recovery window: the three secrets
|
||||
have **fixed names**, and a secret deleted with the default 30-day recovery
|
||||
window keeps its name reserved — a later `terraform apply` would fail with a
|
||||
name conflict until the window elapses. With `deletion_protection = false` the
|
||||
destroy deletes them immediately (`recovery_window_in_days = 0`). If you
|
||||
destroyed a deployment that still had `deletion_protection = true` (or tore
|
||||
down an older copy of this module), clear the scheduled deletions before
|
||||
re-applying:
|
||||
|
||||
```bash
|
||||
for s in gateway-postgres-url gateway-jwt-secret gateway-oidc-client-secret; do
|
||||
aws secretsmanager delete-secret --secret-id "$s" --force-delete-without-recovery
|
||||
done
|
||||
```
|
||||
|
||||
## Guard rails
|
||||
|
||||
Tuned so accidental deletion is hard but greenfield teardown stays easy:
|
||||
|
||||
- `deletion_protection = true` (variable, default true) on RDS and the ALB —
|
||||
blocks accidental deletion; set `false` when you intend to `terraform destroy`.
|
||||
The same switch controls RDS `skip_final_snapshot`, so a protected instance
|
||||
always leaves a final snapshot.
|
||||
- ECR tags are **immutable** and **scanned on push** — a deployed tag can never
|
||||
be silently re-pointed at different bytes. For production, also restrict push
|
||||
rights on the repo to your CI / image-promotion pipeline rather than operator
|
||||
credentials.
|
||||
- The IAM roles carry only the walkthrough's least-privilege documents: Bedrock
|
||||
invoke on the Anthropic model ARNs (task role) and `secretsmanager:GetSecretValue`
|
||||
on exactly the three secrets this module creates (by ARN) plus the AWS-managed
|
||||
ECS execution policy (execution role). Inline policies are scoped to these
|
||||
roles, so nothing else in the account is touched.
|
||||
- TLS everywhere it terminates: the ALB listener pins
|
||||
`ELBSecurityPolicy-TLS13-1-2-2021-06` (no TLS 1.0/1.1), and the store
|
||||
connection uses `sslmode=verify-full` against the RDS CA bundle baked into
|
||||
the image, with `rds.force_ssl=1` enforcing TLS server-side.
|
||||
|
||||
## Private access
|
||||
|
||||
The ALB is **internal** with `ip_address_type = "ipv4"` (a dual-stack internal
|
||||
ALB publishes public-range AAAA records, which the CLI's `/login`
|
||||
private-network check rejects), and its security group admits only
|
||||
`corporate_cidr` on 443. Reaching it from on-prem requires your existing
|
||||
routing into the VPC (Direct Connect / VPN) — **operator / network-team-owned**
|
||||
plumbing this module does not create.
|
||||
|
||||
After the apply, give developers a privately resolvable hostname: in a Route 53
|
||||
private hosted zone, alias the host of `gateway.yaml`'s `public_url` to the ALB
|
||||
(`alb_dns_name` / `alb_zone_id` outputs). The ALB's own `*.elb.amazonaws.com`
|
||||
name can't carry your ACM certificate, so use your own name.
|
||||
|
||||
The tasks run in the private subnets with no public IP; all egress (Bedrock,
|
||||
the IdP, Secrets Manager, ECR, CloudWatch Logs) goes through the NAT gateway.
|
||||
To keep Bedrock traffic off the public path, create a `bedrock-runtime`
|
||||
interface VPC endpoint and point the upstream's `base_url` at it (see
|
||||
`../gateway.yaml.example`); the IdP still needs internet egress.
|
||||
|
||||
## Remote state (recommended for teams)
|
||||
|
||||
Add a backend so state is shared and locked (and out of git):
|
||||
|
||||
```hcl
|
||||
# backend.tf
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "<your-tf-state-bucket>"
|
||||
key = "claude-gateway/ecs"
|
||||
region = "us-east-1"
|
||||
use_lockfile = true # S3-native locking (Terraform >= 1.10); or set dynamodb_table
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## After deploy
|
||||
|
||||
- `terraform output alb_dns_name` / `alb_zone_id` — create the Route 53 alias.
|
||||
- Register `<public_url>/oauth/callback` on the Okta OIDC web app and make sure
|
||||
`../gateway.yaml` `public_url` matches the host you aliased.
|
||||
- Notes: Terraform does not build the image. To ship a new gateway version **or
|
||||
a config edit**, rerun the docker build/push under a new tag and bump
|
||||
`image_tag` — secrets-only rotations roll the service without a rebuild (the
|
||||
task definition stamps a hash of the managed secret values), but a
|
||||
`gateway.yaml` edit reaches the container only through the rebuilt image.
|
||||
@@ -0,0 +1,510 @@
|
||||
# Claude apps gateway on ECS Fargate — Terraform equivalent of setup.sh.
|
||||
# Section markers (§N) map to setup.sh and the walkthrough:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-on-aws
|
||||
#
|
||||
# Unlike the GCP example this module does NOT create the network — the VPC and
|
||||
# private subnets are walkthrough prerequisites, passed in as variables.
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
data "aws_region" "current" {}
|
||||
|
||||
# Read (not created) so a typo'd VPC or subnet ID fails the plan up front
|
||||
# instead of half-applying.
|
||||
data "aws_vpc" "this" {
|
||||
id = var.vpc_id
|
||||
}
|
||||
|
||||
data "aws_subnet" "private" {
|
||||
for_each = toset(var.private_subnet_ids)
|
||||
id = each.value
|
||||
}
|
||||
|
||||
locals {
|
||||
config_path = var.gateway_config_path != "" ? var.gateway_config_path : "${path.module}/../gateway.yaml"
|
||||
gateway_config = file(local.config_path)
|
||||
image = "${aws_ecr_repository.repo.repository_url}:${var.image_tag}"
|
||||
}
|
||||
|
||||
# ── 1 Security groups ───────────────────────────────────────────────────────
|
||||
# Three groups chain the traffic path: corp network -> ALB :443, ALB ->
|
||||
# gateway :8080, gateway -> Postgres :5432. Nothing else is reachable.
|
||||
# Rules are separate resources (not inline) so they never fight other tooling.
|
||||
resource "aws_security_group" "alb" {
|
||||
name = "claude-gateway-alb"
|
||||
description = "Claude gateway ALB"
|
||||
vpc_id = var.vpc_id
|
||||
}
|
||||
|
||||
resource "aws_security_group" "gateway" {
|
||||
name = "claude-gateway-svc"
|
||||
description = "Claude gateway service"
|
||||
vpc_id = var.vpc_id
|
||||
}
|
||||
|
||||
resource "aws_security_group" "db" {
|
||||
name = "claude-gateway-db"
|
||||
description = "Claude gateway Postgres"
|
||||
vpc_id = var.vpc_id
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "alb_https" {
|
||||
security_group_id = aws_security_group.alb.id
|
||||
description = "HTTPS from the corporate network"
|
||||
ip_protocol = "tcp"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
cidr_ipv4 = var.corporate_cidr
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "gateway_from_alb" {
|
||||
security_group_id = aws_security_group.gateway.id
|
||||
description = "Gateway port from the ALB"
|
||||
ip_protocol = "tcp"
|
||||
from_port = 8080
|
||||
to_port = 8080
|
||||
referenced_security_group_id = aws_security_group.alb.id
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "db_from_gateway" {
|
||||
security_group_id = aws_security_group.db.id
|
||||
description = "Postgres from the gateway"
|
||||
ip_protocol = "tcp"
|
||||
from_port = 5432
|
||||
to_port = 5432
|
||||
referenced_security_group_id = aws_security_group.gateway.id
|
||||
}
|
||||
|
||||
# Egress: the ALB only needs to reach its targets; the gateway needs the NAT
|
||||
# path out (Bedrock, the IdP, Secrets Manager, ECR, CloudWatch Logs) plus
|
||||
# Postgres. The DB group needs no egress (security groups are stateful).
|
||||
resource "aws_vpc_security_group_egress_rule" "alb_to_gateway" {
|
||||
security_group_id = aws_security_group.alb.id
|
||||
description = "Health checks + forwarding to gateway tasks"
|
||||
ip_protocol = "tcp"
|
||||
from_port = 8080
|
||||
to_port = 8080
|
||||
referenced_security_group_id = aws_security_group.gateway.id
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_egress_rule" "gateway_all" {
|
||||
security_group_id = aws_security_group.gateway.id
|
||||
description = "Egress to Bedrock, the IdP, Secrets Manager, ECR, CloudWatch Logs, Postgres"
|
||||
ip_protocol = "-1"
|
||||
cidr_ipv4 = "0.0.0.0/0"
|
||||
}
|
||||
|
||||
# ── 2 IAM roles (least-privilege) ───────────────────────────────────────────
|
||||
# Task role: the gateway's runtime identity. Its ONLY permission is invoking
|
||||
# Claude models on Bedrock — the upstream's `auth: {}` resolves to this role
|
||||
# via the AWS default credential chain. The policy must cover both the
|
||||
# cross-region inference-profile ARNs and the underlying foundation-model ARNs.
|
||||
data "aws_iam_policy_document" "ecs_trust" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRole"]
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["ecs-tasks.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "task" {
|
||||
name = var.task_role_name
|
||||
assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "bedrock_invoke" {
|
||||
name = "bedrock-invoke"
|
||||
role = aws_iam_role.task.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
|
||||
Resource = [
|
||||
"arn:aws:bedrock:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:inference-profile/us.anthropic.*",
|
||||
"arn:aws:bedrock:*::foundation-model/anthropic.*",
|
||||
]
|
||||
}]
|
||||
})
|
||||
|
||||
# The walkthrough is scoped to commercial US regions: this policy and the
|
||||
# gateway's built-in model catalog both use the us.anthropic.* geo-prefixed
|
||||
# cross-region inference profiles, which only exist in the commercial US
|
||||
# regions — an explicit list, not a `us-` prefix match, because GovCloud
|
||||
# (us-gov-*) and ISO (us-iso-*) regions share the prefix but live in
|
||||
# different AWS partitions where those profiles and this module's arn:aws:
|
||||
# ARNs are wrong. Anywhere else the deploy provisions fine and then every
|
||||
# model call fails. Other-region deploys must pin region-appropriate
|
||||
# profiles via a models: block in gateway.yaml (see the config reference's
|
||||
# models: guidance: https://code.claude.com/docs/en/claude-apps-gateway-config),
|
||||
# widen the inference-profile ARN geo prefix above, and set
|
||||
# allow_non_us_region = true.
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = var.allow_non_us_region || contains(["us-east-1", "us-east-2", "us-west-1", "us-west-2"], var.region)
|
||||
error_message = "region is not a commercial US region (GovCloud/ISO share the us- prefix but are different partitions), and this module's IAM policy and the built-in model catalog use the US-geo (us.anthropic.*) inference profiles. Pin your region's inference profiles in a models: block in gateway.yaml, adjust the bedrock-invoke ARN prefix, then set allow_non_us_region = true."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Execution role: the ECS agent's identity — pulls the image from ECR and
|
||||
# injects the Secrets Manager values into the container; the gateway never
|
||||
# uses it. AmazonECSTaskExecutionRolePolicy covers the ECR pull + awslogs;
|
||||
# the inline policy adds read on exactly the three secrets this module
|
||||
# creates — their full ARNs, not a name-prefix wildcard, so nothing else
|
||||
# in a shared account (present or future) is readable through this role.
|
||||
resource "aws_iam_role" "execution" {
|
||||
name = var.execution_role_name
|
||||
assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "execution_managed" {
|
||||
role = aws_iam_role.execution.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "secrets_read" {
|
||||
name = "read-gateway-secrets"
|
||||
role = aws_iam_role.execution.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Action = "secretsmanager:GetSecretValue"
|
||||
Resource = [
|
||||
aws_secretsmanager_secret.jwt.arn,
|
||||
aws_secretsmanager_secret.oidc.arn,
|
||||
aws_secretsmanager_secret.postgres_url.arn,
|
||||
]
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
# ── 6 ECR repository ────────────────────────────────────────────────────────
|
||||
# NOTE: image build/push is a separate step (see README) — Terraform only makes
|
||||
# the repo. IMMUTABLE tags + scan-on-push: the ECS service pulls whatever this
|
||||
# repo serves under the deployed tag, so a pushed tag must never be silently
|
||||
# re-pointed. For production, also restrict push rights on this repo to your
|
||||
# CI / image-promotion pipeline rather than operator credentials.
|
||||
resource "aws_ecr_repository" "repo" {
|
||||
name = var.ecr_repo
|
||||
image_tag_mutability = "IMMUTABLE"
|
||||
image_scanning_configuration {
|
||||
scan_on_push = true
|
||||
}
|
||||
}
|
||||
|
||||
# ── 3 RDS for PostgreSQL (private subnets, no public address) ───────────────
|
||||
resource "aws_db_subnet_group" "db" {
|
||||
name = var.db_instance
|
||||
description = "Claude gateway"
|
||||
subnet_ids = var.private_subnet_ids
|
||||
}
|
||||
|
||||
# rds.force_ssl: reject plaintext connections server-side — the client-side
|
||||
# counterpart is sslmode=verify-full in the connection string (§5). The family
|
||||
# tracks the major version in var.db_engine_version.
|
||||
#
|
||||
# name_prefix + create_before_destroy: a major engine bump changes `family`,
|
||||
# which forces replacement — with a static name that deadlocks (the new group
|
||||
# can't be created under the taken name; the old can't be destroyed while the
|
||||
# live instance uses it: "parameter group is currently in use"). With this
|
||||
# shape the replacement group gets a fresh unique name, the instance is
|
||||
# repointed, then the old group is destroyed. (The subnet group above needs
|
||||
# neither: subnet_ids update in place and an engine bump never touches it.)
|
||||
resource "aws_db_parameter_group" "db" {
|
||||
name_prefix = "${var.db_instance}-"
|
||||
family = "postgres${split(".", var.db_engine_version)[0]}"
|
||||
description = "Claude gateway - require TLS on every connection"
|
||||
|
||||
parameter {
|
||||
name = "rds.force_ssl"
|
||||
value = "1"
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
# URL-safe (alphanumeric) so it drops cleanly into the connection string.
|
||||
# nosemgrep: terraform-generic-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "random_password" "db" {
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-aws-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "aws_db_instance" "db" {
|
||||
identifier = var.db_instance
|
||||
engine = "postgres"
|
||||
engine_version = var.db_engine_version
|
||||
instance_class = var.db_instance_class
|
||||
allocated_storage = var.db_allocated_storage
|
||||
db_name = var.db_name
|
||||
username = var.db_user
|
||||
password = random_password.db.result
|
||||
db_subnet_group_name = aws_db_subnet_group.db.name
|
||||
parameter_group_name = aws_db_parameter_group.db.name
|
||||
vpc_security_group_ids = [aws_security_group.db.id]
|
||||
publicly_accessible = false
|
||||
storage_encrypted = true
|
||||
deletion_protection = var.deletion_protection
|
||||
# Greenfield teardown: skip the final snapshot only once deletion protection
|
||||
# is deliberately turned off (the same switch — see README "Tear down").
|
||||
skip_final_snapshot = !var.deletion_protection
|
||||
final_snapshot_identifier = "${var.db_instance}-final"
|
||||
}
|
||||
|
||||
# ── 5 Secrets Manager ───────────────────────────────────────────────────────
|
||||
# postgres-url: connection string built from the instance's private endpoint.
|
||||
# The execution role's policy (§2) grants read on these three secrets' ARNs
|
||||
# and nothing else.
|
||||
#
|
||||
# recovery_window_in_days rides the same switch as skip_final_snapshot: the
|
||||
# secrets have fixed names, so a destroy that leaves them in the default
|
||||
# 30-day scheduled-deletion state makes the next apply fail with a name
|
||||
# conflict. Greenfield teardown (deletion_protection = false) deletes them
|
||||
# immediately; a protected deployment keeps the 30-day recovery window.
|
||||
resource "aws_secretsmanager_secret" "postgres_url" {
|
||||
name = var.secret_name
|
||||
recovery_window_in_days = var.deletion_protection ? 30 : 0
|
||||
}
|
||||
|
||||
# sslmode=verify-full: the gateway's driver (Bun.SQL) honors sslmode from the
|
||||
# URL and verifies the server certificate chain AND hostname. The trust anchor
|
||||
# is the AWS RDS CA bundle baked into the image at /etc/claude/rds-global-bundle.pem
|
||||
# and loaded via NODE_EXTRA_CA_CERTS (see ../Dockerfile) — do NOT add a
|
||||
# libpq-style `sslrootcert=` query param: the driver doesn't read it and
|
||||
# forwards it to Postgres as a startup parameter, which the server rejects.
|
||||
# nosemgrep: terraform-aws-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "aws_secretsmanager_secret_version" "postgres_url" {
|
||||
secret_id = aws_secretsmanager_secret.postgres_url.id
|
||||
secret_string = "postgres://${var.db_user}:${random_password.db.result}@${aws_db_instance.db.address}:5432/${var.db_name}?sslmode=verify-full"
|
||||
}
|
||||
|
||||
# jwt: session signing key.
|
||||
# nosemgrep: terraform-generic-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "random_password" "jwt" {
|
||||
length = 48
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret" "jwt" {
|
||||
name = var.jwt_secret_name
|
||||
recovery_window_in_days = var.deletion_protection ? 30 : 0 # see postgres_url
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-aws-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "aws_secretsmanager_secret_version" "jwt" {
|
||||
secret_id = aws_secretsmanager_secret.jwt.id
|
||||
secret_string = random_password.jwt.result
|
||||
}
|
||||
|
||||
# oidc client secret: operator-provided (from the Okta OIDC web app).
|
||||
resource "aws_secretsmanager_secret" "oidc" {
|
||||
name = var.oidc_secret_name
|
||||
recovery_window_in_days = var.deletion_protection ? 30 : 0 # see postgres_url
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-aws-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "aws_secretsmanager_secret_version" "oidc" {
|
||||
count = var.oidc_client_secret != "" ? 1 : 0
|
||||
secret_id = aws_secretsmanager_secret.oidc.id
|
||||
secret_string = var.oidc_client_secret
|
||||
}
|
||||
|
||||
# Warn (not block) at plan time when the OIDC secret value isn't set: the task
|
||||
# definition references the secret unconditionally, so an empty value with no
|
||||
# out-of-band version means the tasks fail to start late, at container init
|
||||
# (ResourceInitializationError). A warning (not a precondition) keeps the
|
||||
# documented out-of-band-version mode usable.
|
||||
check "oidc_client_secret_set" {
|
||||
assert {
|
||||
condition = var.oidc_client_secret != ""
|
||||
error_message = "oidc_client_secret is empty — set it in terraform.tfvars, or add a version to the gateway-oidc-client-secret secret out-of-band before applying (the ECS tasks inject it at start and will fail without one)."
|
||||
}
|
||||
}
|
||||
|
||||
# ── 7 ECS Fargate service + internal ALB ────────────────────────────────────
|
||||
resource "aws_ecs_cluster" "cluster" {
|
||||
name = var.cluster_name
|
||||
}
|
||||
|
||||
# The gateway's stderr carries both its audit events and operational logs.
|
||||
# Bounded retention — without it the group keeps logs forever and cost grows
|
||||
# unbounded; the default (90 days) is sized for audit-trail review windows.
|
||||
resource "aws_cloudwatch_log_group" "gateway" {
|
||||
name = var.log_group_name
|
||||
retention_in_days = var.log_retention_days
|
||||
}
|
||||
|
||||
# Task definition. gateway.yaml ships INSIDE the image (unlike the GCP example,
|
||||
# which mounts it from Secret Manager), so Terraform reads ../gateway.yaml only
|
||||
# to (a) enforce the no-REPLACE_ME guard before a deploy and (b) stamp a hash
|
||||
# of the config + every managed secret value into the container environment —
|
||||
# secrets are injected at task start, so rotating one (tainting
|
||||
# random_password.db ALTERs the DB password; a new oidc_client_secret) would
|
||||
# otherwise leave running tasks on stale values with nothing forcing a roll.
|
||||
# NOTE the hash only forces a roll; a config EDIT still reaches the container
|
||||
# only via a rebuilt image — push under a new tag (the repo enforces
|
||||
# immutability) and bump image_tag, or the roll redeploys the old config.
|
||||
resource "aws_ecs_task_definition" "gateway" {
|
||||
family = var.service_name
|
||||
network_mode = "awsvpc"
|
||||
requires_compatibilities = ["FARGATE"]
|
||||
cpu = tostring(var.task_cpu)
|
||||
memory = tostring(var.task_memory)
|
||||
execution_role_arn = aws_iam_role.execution.arn
|
||||
task_role_arn = aws_iam_role.task.arn
|
||||
|
||||
runtime_platform {
|
||||
cpu_architecture = "X86_64" # build the image linux/amd64; ARM64 for Graviton (see ../Dockerfile)
|
||||
operating_system_family = "LINUX"
|
||||
}
|
||||
|
||||
container_definitions = jsonencode([
|
||||
{
|
||||
name = "gateway"
|
||||
image = local.image
|
||||
portMappings = [{ containerPort = 8080 }]
|
||||
environment = [
|
||||
{
|
||||
name = "GATEWAY_CONFIG_SHA"
|
||||
value = substr(sha256(join("", [
|
||||
local.gateway_config,
|
||||
random_password.db.result,
|
||||
random_password.jwt.result,
|
||||
var.oidc_client_secret,
|
||||
])), 0, 16)
|
||||
},
|
||||
]
|
||||
secrets = [
|
||||
{ name = "GATEWAY_JWT_SECRET", valueFrom = aws_secretsmanager_secret.jwt.arn },
|
||||
{ name = "OIDC_CLIENT_SECRET", valueFrom = aws_secretsmanager_secret.oidc.arn },
|
||||
{ name = "GATEWAY_POSTGRES_URL", valueFrom = aws_secretsmanager_secret.postgres_url.arn },
|
||||
]
|
||||
logConfiguration = {
|
||||
logDriver = "awslogs"
|
||||
options = {
|
||||
awslogs-group = aws_cloudwatch_log_group.gateway.name
|
||||
awslogs-region = data.aws_region.current.region
|
||||
awslogs-stream-prefix = "gateway"
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
# Guard mirrors setup.sh's REPLACE_ME check (non-comment lines): the config
|
||||
# is baked into the image this task definition deploys, so a half-filled
|
||||
# gateway.yaml at apply time means the pushed image is half-filled too.
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = length([
|
||||
for line in split("\n", local.gateway_config) :
|
||||
line
|
||||
if !startswith(trimspace(line), "#") && strcontains(line, "REPLACE_ME")
|
||||
]) == 0
|
||||
error_message = "gateway.yaml still has REPLACE_ME on a non-comment line — fill it in (and rebuild/push the image) before applying."
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
aws_secretsmanager_secret_version.postgres_url,
|
||||
aws_secretsmanager_secret_version.jwt,
|
||||
]
|
||||
}
|
||||
|
||||
# Internal ALB. ip_address_type ipv4: an internal dual-stack ALB publishes
|
||||
# public-range AAAA records, which the CLI's /login private-network check
|
||||
# rejects. idle_timeout 3600: the 60-second default closes a streaming
|
||||
# response at the first quiet period (long prompt processing before the first
|
||||
# token, extended thinking with no streamed output).
|
||||
resource "aws_lb" "gateway" {
|
||||
name = var.service_name
|
||||
internal = true
|
||||
load_balancer_type = "application"
|
||||
ip_address_type = "ipv4"
|
||||
subnets = var.private_subnet_ids
|
||||
security_groups = [aws_security_group.alb.id]
|
||||
idle_timeout = 3600
|
||||
enable_deletion_protection = var.deletion_protection
|
||||
}
|
||||
|
||||
# /readyz verifies the store is reachable, so a task that can't reach Postgres
|
||||
# never enters rotation (the gateway also serves liveness-only /healthz — see
|
||||
# the deploy guide's outage-behavior tradeoff).
|
||||
resource "aws_lb_target_group" "gateway" {
|
||||
name = var.service_name
|
||||
protocol = "HTTP"
|
||||
port = 8080
|
||||
vpc_id = var.vpc_id
|
||||
target_type = "ip"
|
||||
|
||||
health_check {
|
||||
path = "/readyz"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_lb_listener" "https" {
|
||||
load_balancer_arn = aws_lb.gateway.arn
|
||||
protocol = "HTTPS"
|
||||
port = 443
|
||||
# Explicit modern policy — omitting ssl_policy falls back to the legacy
|
||||
# ELBSecurityPolicy-2016-08 default, which still accepts TLS 1.0/1.1.
|
||||
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
|
||||
certificate_arn = var.acm_certificate_arn
|
||||
|
||||
default_action {
|
||||
type = "forward"
|
||||
target_group_arn = aws_lb_target_group.gateway.arn
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_ecs_service" "gateway" {
|
||||
name = var.service_name
|
||||
cluster = aws_ecs_cluster.cluster.id
|
||||
task_definition = aws_ecs_task_definition.gateway.arn
|
||||
desired_count = var.desired_count
|
||||
launch_type = "FARGATE"
|
||||
|
||||
# Stop a rollout whose tasks keep failing (bad image, unbootable config) and
|
||||
# roll back to the last steady state instead of relaunching failing tasks
|
||||
# forever.
|
||||
deployment_circuit_breaker {
|
||||
enable = true
|
||||
rollback = true
|
||||
}
|
||||
|
||||
network_configuration {
|
||||
subnets = var.private_subnet_ids
|
||||
security_groups = [aws_security_group.gateway.id]
|
||||
# All egress (Bedrock, the IdP, Secrets Manager, ECR, CloudWatch Logs)
|
||||
# goes through the NAT gateway — tasks get no public IP.
|
||||
assign_public_ip = false
|
||||
}
|
||||
|
||||
load_balancer {
|
||||
target_group_arn = aws_lb_target_group.gateway.arn
|
||||
container_name = "gateway"
|
||||
container_port = 8080
|
||||
}
|
||||
|
||||
# Tasks register with the ALB at start — give a cold task (image pull +
|
||||
# store connect + first /readyz) time before ECS replaces it as unhealthy.
|
||||
health_check_grace_period_seconds = 60
|
||||
|
||||
# The listener must exist before targets register; the secrets must be
|
||||
# readable before the first task starts.
|
||||
depends_on = [
|
||||
aws_lb_listener.https,
|
||||
aws_iam_role_policy.secrets_read,
|
||||
aws_iam_role_policy_attachment.execution_managed,
|
||||
aws_secretsmanager_secret_version.postgres_url,
|
||||
aws_secretsmanager_secret_version.jwt,
|
||||
aws_secretsmanager_secret_version.oidc,
|
||||
aws_db_instance.db,
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
output "alb_dns_name" {
|
||||
description = "Internal ALB DNS name. Alias your gateway hostname (the host in gateway.yaml's public_url) to this in a Route 53 private hosted zone — the *.elb.amazonaws.com name itself can't carry your ACM certificate."
|
||||
value = aws_lb.gateway.dns_name
|
||||
}
|
||||
|
||||
output "alb_zone_id" {
|
||||
description = "ALB hosted zone ID, for the Route 53 alias record."
|
||||
value = aws_lb.gateway.zone_id
|
||||
}
|
||||
|
||||
output "image" {
|
||||
description = "Image the service runs (build/push this separately — see README)."
|
||||
value = local.image
|
||||
}
|
||||
|
||||
output "ecr_repository_url" {
|
||||
description = "ECR repository URL to push the gateway image to."
|
||||
value = aws_ecr_repository.repo.repository_url
|
||||
}
|
||||
|
||||
output "task_role_arn" {
|
||||
description = "Gateway runtime task role (Bedrock invoke)."
|
||||
value = aws_iam_role.task.arn
|
||||
}
|
||||
|
||||
output "execution_role_arn" {
|
||||
description = "ECS execution role (image pull + secret injection)."
|
||||
value = aws_iam_role.execution.arn
|
||||
}
|
||||
|
||||
output "db_endpoint" {
|
||||
description = "RDS private endpoint (host only; the connection string lives in the gateway-postgres-url secret)."
|
||||
value = aws_db_instance.db.address
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copy to terraform.tfvars and edit. terraform.tfvars is gitignored (see .gitignore).
|
||||
|
||||
region = "us-east-1" # a region where Bedrock serves the Claude models you need
|
||||
|
||||
# Prerequisite networking (NOT created by this module): the VPC and two+ private
|
||||
# subnets in different AZs with outbound internet via a NAT gateway.
|
||||
vpc_id = "vpc-..."
|
||||
private_subnet_ids = ["subnet-...a", "subnet-...b"]
|
||||
|
||||
# The only source the ALB admits on 443. Must not overlap the private subnets
|
||||
# above — hosts in the ALB subnets are trusted_proxies (gateway.yaml) and could
|
||||
# spoof client IPs via X-Forwarded-For.
|
||||
corporate_cidr = "10.0.0.0/8"
|
||||
|
||||
# ACM certificate for your internal gateway hostname (the host in gateway.yaml's
|
||||
# public_url), imported or issued by AWS Private CA.
|
||||
acm_certificate_arn = "arn:aws:acm:..."
|
||||
|
||||
image_tag = "<version>" # REQUIRED — the tag you build and push as linux/amd64 with
|
||||
# gateway.yaml baked in (setup.sh tags <version>-cfg<sha8>;
|
||||
# see README Deploy)
|
||||
|
||||
# Okta OIDC client secret: REQUIRED — uncomment and set it (Terraform creates the
|
||||
# secret version; the ECS tasks inject `gateway-oidc-client-secret` at start, so
|
||||
# without a version they fail with ResourceInitializationError). Leave empty only
|
||||
# if you add the secret version out-of-band.
|
||||
# oidc_client_secret = "..."
|
||||
@@ -0,0 +1,189 @@
|
||||
# Inputs — mirror the env-overridable knobs in setup.sh (same defaults).
|
||||
|
||||
variable "region" {
|
||||
description = "AWS region for everything this module creates. Pick one where Bedrock serves the Claude models you need. (The Bedrock region the gateway calls is set separately inside gateway.yaml — keep the two equal.) The walkthrough is scoped to the commercial US regions (us-east-1/us-east-2/us-west-1/us-west-2 — GovCloud and ISO regions are different partitions); see allow_non_us_region."
|
||||
type = string
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
variable "allow_non_us_region" {
|
||||
description = "The bedrock-invoke IAM policy and the gateway's built-in model catalog use the US-geo (us.anthropic.*) cross-region inference profiles, so any region outside the commercial US four (including GovCloud/ISO, which are different partitions) fails a plan-time precondition. Set true ONLY after pinning region-appropriate inference profiles via a models: block in gateway.yaml (see the config reference) and widening the ARN geo prefix in main.tf's bedrock-invoke policy."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
# ── Networking inputs (prerequisites — NOT created here) ────────────────────
|
||||
variable "vpc_id" {
|
||||
description = "Existing VPC ID (the walkthrough's prerequisite VPC). Unlike the GCP example, this module does not create the network."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "private_subnet_ids" {
|
||||
description = "Two+ private subnet IDs in different AZs, with outbound internet via a NAT gateway. The internal ALB, the ECS tasks, and the RDS subnet group all attach here."
|
||||
type = list(string)
|
||||
validation {
|
||||
condition = length(var.private_subnet_ids) >= 2
|
||||
error_message = "private_subnet_ids needs at least two subnets in different AZs (the internal ALB requires two)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "corporate_cidr" {
|
||||
description = "Your corporate network CIDR — the only source the ALB security group admits on 443. Must not overlap private_subnet_ids: hosts there are trusted_proxies (gateway.yaml) and could spoof client IPs via X-Forwarded-For."
|
||||
type = string
|
||||
}
|
||||
|
||||
# ── IAM (§2) ────────────────────────────────────────────────────────────────
|
||||
variable "task_role_name" {
|
||||
description = "ECS task role name (the gateway's runtime identity; its only permission is Bedrock invoke)."
|
||||
type = string
|
||||
default = "claude-gateway-task"
|
||||
}
|
||||
|
||||
variable "execution_role_name" {
|
||||
description = "ECS execution role name (the ECS agent's identity: pulls the image, injects the secrets)."
|
||||
type = string
|
||||
default = "claude-gateway-execution"
|
||||
}
|
||||
|
||||
# ── Image (§6) ──────────────────────────────────────────────────────────────
|
||||
# Terraform creates the ECR repository but does NOT build/push the image (that's
|
||||
# a docker build step — see README). It references the image by tag.
|
||||
variable "ecr_repo" {
|
||||
description = "ECR repository name."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Image tag — the tag you built and pushed (must already exist in the repo as linux/amd64, with gateway.yaml baked in). setup.sh tags as <version>-cfg<sha8 of gateway.yaml>; see the README Deploy section for the build command."
|
||||
type = string
|
||||
validation {
|
||||
condition = can(regex("^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$", var.image_tag))
|
||||
error_message = "image_tag must be a valid OCI tag — set it to the tag you pushed (the '<version>' in terraform.tfvars.example is a placeholder)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "gateway_config_path" {
|
||||
description = "Path to gateway.yaml. Empty = ../gateway.yaml relative to this module. Read for the REPLACE_ME guard and the config-sha that rolls the service; the file itself ships inside the image."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ── RDS (§3) ────────────────────────────────────────────────────────────────
|
||||
variable "db_instance" {
|
||||
description = "RDS instance identifier."
|
||||
type = string
|
||||
default = "claude-gateway-db"
|
||||
}
|
||||
|
||||
variable "db_engine_version" {
|
||||
description = "Postgres major version. The gateway supports PostgreSQL 14 or newer; 16 is the recommended default."
|
||||
type = string
|
||||
default = "16"
|
||||
}
|
||||
|
||||
variable "db_instance_class" {
|
||||
description = "RDS instance class."
|
||||
type = string
|
||||
default = "db.t4g.micro"
|
||||
}
|
||||
|
||||
variable "db_allocated_storage" {
|
||||
description = "RDS allocated storage in GiB."
|
||||
type = number
|
||||
default = 20
|
||||
}
|
||||
|
||||
variable "db_name" {
|
||||
description = "Database name."
|
||||
type = string
|
||||
default = "claude_gateway"
|
||||
}
|
||||
|
||||
variable "db_user" {
|
||||
description = "Database master user (the gateway connects as this role)."
|
||||
type = string
|
||||
default = "gateway"
|
||||
}
|
||||
|
||||
# ── Secrets (§5) ────────────────────────────────────────────────────────────
|
||||
# The execution role's secrets-read policy grants read on exactly these three
|
||||
# secrets' ARNs, so renames are picked up automatically on the next apply.
|
||||
variable "secret_name" {
|
||||
description = "Secrets Manager secret holding the Postgres connection string."
|
||||
type = string
|
||||
default = "gateway-postgres-url"
|
||||
}
|
||||
|
||||
variable "jwt_secret_name" {
|
||||
description = "Secrets Manager secret holding the session JWT signing key."
|
||||
type = string
|
||||
default = "gateway-jwt-secret"
|
||||
}
|
||||
|
||||
variable "oidc_secret_name" {
|
||||
description = "Secrets Manager secret holding the Okta OIDC client secret."
|
||||
type = string
|
||||
default = "gateway-oidc-client-secret"
|
||||
}
|
||||
|
||||
variable "oidc_client_secret" {
|
||||
description = "Okta OIDC client secret value. Leave empty to NOT manage the version via Terraform (only if you add the secret version out-of-band — without one the tasks fail to start)."
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# ── ECS + ALB (§7) ──────────────────────────────────────────────────────────
|
||||
variable "cluster_name" {
|
||||
description = "ECS cluster name."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
variable "service_name" {
|
||||
description = "ECS service name (also used for the ALB and target group)."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
variable "log_group_name" {
|
||||
description = "CloudWatch Logs group for the gateway's stderr (audit events + operational logs)."
|
||||
type = string
|
||||
default = "/ecs/claude-gateway"
|
||||
}
|
||||
|
||||
variable "log_retention_days" {
|
||||
description = "CloudWatch Logs retention in days. The group carries the gateway's audit events, so align with your audit retention policy."
|
||||
type = number
|
||||
default = 90
|
||||
}
|
||||
|
||||
variable "acm_certificate_arn" {
|
||||
description = "ACM certificate ARN for the internal gateway hostname (the host in gateway.yaml's public_url), served by the ALB's HTTPS listener."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "task_cpu" {
|
||||
description = "Fargate task CPU units."
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
variable "task_memory" {
|
||||
description = "Fargate task memory (MiB)."
|
||||
type = number
|
||||
default = 2048
|
||||
}
|
||||
|
||||
variable "desired_count" {
|
||||
description = "ECS service desired task count. Each task opens a Postgres pool of up to 5 connections (the gateway's store.max_connections default) and db.t4g.micro caps at ~80 max_connections — keep desired_count × 5 below the DB class's limit, or raise the class before raising this."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "deletion_protection" {
|
||||
description = "Deletion protection on RDS and the ALB (and whether RDS skips the final snapshot on destroy). Keep true to avoid accidental deletion of the running deployment."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Provider + version pins for the Claude apps gateway ECS Fargate deployment.
|
||||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 6.0, < 7.0" # 6.0 renames data.aws_region's attribute to `region`
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = ">= 3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Keep the build context to just the binary the Dockerfile COPYs. BuildKit (the
|
||||
# default, selected via the Dockerfile's syntax directive) only syncs the
|
||||
# referenced COPY source anyway, so this is a no-op there — it matters for the
|
||||
# classic builder (DOCKER_BUILDKIT=0) and as a conventional signal that the
|
||||
# .gitignore'd secrets in this directory aren't part of the image build.
|
||||
terraform/
|
||||
**/.terraform/
|
||||
*.tfstate*
|
||||
terraform.tfvars
|
||||
gateway.yaml
|
||||
secrets/
|
||||
*.pem
|
||||
client_secret_*.json
|
||||
@@ -0,0 +1,12 @@
|
||||
# Local, environment-specific config — copy gateway.yaml.example -> gateway.yaml
|
||||
# (gateway.yaml.example IS committed; your filled-in gateway.yaml is not)
|
||||
gateway.yaml
|
||||
|
||||
# Secrets / credentials — never commit
|
||||
secrets/
|
||||
client_secret_*.json
|
||||
*.pem
|
||||
|
||||
# Release binary and pinned version — setup.sh downloads/writes these per release
|
||||
claude
|
||||
.claude-version
|
||||
@@ -0,0 +1,35 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Runtime image for `claude gateway`.
|
||||
#
|
||||
# This image does NOT build the binary. It expects a prebuilt native
|
||||
# linux-x64 `claude` executable in the build context — the public Claude Code
|
||||
# release binary, which includes the `gateway` subcommand. setup.sh places it
|
||||
# at ./claude (downloading it from the public release endpoint and verifying
|
||||
# it against the release manifest if missing). Override CLAUDE_BINARY to
|
||||
# point at a different path.
|
||||
#
|
||||
# Build (with the binary at ./claude; otherwise add --build-arg CLAUDE_BINARY=<path>):
|
||||
# docker build --platform=linux/amd64 --provenance=false -t claude-gateway .
|
||||
#
|
||||
# Run:
|
||||
# docker run --rm -p 8080:8080 \
|
||||
# -v "$PWD/gateway.yaml:/etc/claude/gateway.yaml:ro" \
|
||||
# -e OIDC_CLIENT_SECRET -e GATEWAY_JWT_SECRET -e GATEWAY_POSTGRES_URL \
|
||||
# claude-gateway
|
||||
|
||||
ARG CLAUDE_BINARY=./claude
|
||||
|
||||
# distroless/cc provides glibc + libstdc++ (required by the Bun-compiled
|
||||
# native binary). The :nonroot tag runs as uid/gid 65532.
|
||||
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||
|
||||
ARG CLAUDE_BINARY
|
||||
COPY --chmod=0755 ${CLAUDE_BINARY} /usr/local/bin/claude
|
||||
|
||||
ENV CLAUDE_CONFIG_DIR=/tmp/.claude
|
||||
|
||||
EXPOSE 8080
|
||||
USER nonroot
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/claude", "gateway", "--config", "/etc/claude/gateway.yaml"]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Claude Gateway on Google Cloud
|
||||
|
||||
Reference deployment artifacts for running Claude Gateway on GCP with Agent
|
||||
Platform (formerly Vertex AI) as the upstream: Cloud Run or GKE, Cloud SQL for
|
||||
PostgreSQL, Secret Manager, and service-account auth to Agent Platform.
|
||||
|
||||
These files are provided as a working example rather than a supported production
|
||||
deployment. Adapt them to your own environment.
|
||||
|
||||
- **Walkthrough**: https://code.claude.com/docs/en/claude-apps-gateway-on-gcp
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `setup.sh` | Scripts the walkthrough end to end via `gcloud` |
|
||||
| `Dockerfile` | Runtime image for the `claude gateway` binary |
|
||||
| `gateway.yaml.example` | Gateway config template, GCP-shaped (Agent Platform upstream, Google Workspace IdP) |
|
||||
| `terraform/` | Provisions the full architecture (two-pass apply — see `terraform/README.md`) |
|
||||
@@ -0,0 +1,156 @@
|
||||
# gateway.yaml.example — Claude Gateway config template, GCP-shaped (walkthrough §6).
|
||||
#
|
||||
# Google Workspace IdP + Agent Platform (formerly Vertex AI) upstream, following
|
||||
# the walkthrough at https://code.claude.com/docs/en/claude-apps-gateway-on-gcp.
|
||||
# The active sections
|
||||
# below are a strict subset of the full configuration reference at
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway; optional keys are included
|
||||
# commented-out.
|
||||
#
|
||||
# USAGE — this is the shippable TEMPLATE. Copy it to gateway.yaml and fill it in:
|
||||
# cp gateway.yaml.example gateway.yaml
|
||||
# setup.sh and terraform/ read gateway.yaml (your filled-in copy, which is
|
||||
# gitignored). It is published as the Secret Manager secret `gateway-config`
|
||||
# (§6) and mounted at /etc/claude/gateway.yaml — the container ENTRYPOINT runs
|
||||
# `claude gateway --config /etc/claude/gateway.yaml`.
|
||||
#
|
||||
# Secret expansion: ${ENV_VAR} reads an env var; ${file:/path} reads a mounted file.
|
||||
# On Cloud Run, setup.sh injects the JWT / OIDC / Postgres secrets as ENV VARS
|
||||
# (Cloud Run can't mount multiple secrets into a single directory), and mounts
|
||||
# only gateway.yaml itself as a file at /etc/claude. On GKE you may use file mounts.
|
||||
#
|
||||
# BEFORE DEPLOY — replace every REPLACE_ME placeholder below (setup.sh refuses to
|
||||
# publish the config secret while any remain), and create the referenced secrets:
|
||||
# gateway-jwt-secret (setup.sh generates this)
|
||||
# gateway-oidc-client-secret (from the Google Cloud Console OAuth client)
|
||||
# gateway-postgres-url (setup.sh generates this)
|
||||
|
||||
# ── Listener ─────────────────────────────────────────────────────────────────
|
||||
listen:
|
||||
host: 0.0.0.0
|
||||
port: 8080 # Cloud Run sets PORT=8080; leave as-is
|
||||
# Required. Fixes the IdP redirect_uri, the OIDC discovery doc, and the
|
||||
# gateway-token issuer so none are derived from the client-controlled Host
|
||||
# header (X-Forwarded-Host/-Proto are likewise never trusted). On Cloud Run
|
||||
# the run.app URL is only assigned on the first deploy, so this starts as a
|
||||
# placeholder for the provisioning-only first pass (login does NOT work until
|
||||
# the real URL is set). After the first deploy, setup.sh prints the run.app
|
||||
# URL: set it here (or your LB hostname) and re-run; setup.sh republishes the
|
||||
# config and redeploys. Register the same host's /oauth/callback on the
|
||||
# Google OAuth client.
|
||||
public_url: https://set-after-first-deploy.invalid
|
||||
# Register this exact redirect URI on the Google OAuth client:
|
||||
# https://<public_url host>/oauth/callback
|
||||
#
|
||||
# On Cloud Run (or behind any L7 LB) every request arrives via Google's front
|
||||
# end, so the gateway sees one peer IP for all developers — set trusted_proxies
|
||||
# so X-Forwarded-For from those proxies is trusted and per-IP rate limiting /
|
||||
# audit IPs record the real client. 169.254.0.0/16 is Cloud Run's fixed
|
||||
# link-local serving range; the proxy-only subnet is the one your internal ALB
|
||||
# uses in this VPC.
|
||||
# trusted_proxies:
|
||||
# - 169.254.0.0/16 # Cloud Run serving proxy (link-local peer)
|
||||
# - <proxy-only-subnet-cidr> # add if fronted by your internal ALB (its proxy-only subnet)
|
||||
#
|
||||
# Alternative — terminate TLS in the gateway itself instead of at a proxy:
|
||||
# tls:
|
||||
# cert: /certs/gateway.crt
|
||||
# key: /certs/gateway.key
|
||||
|
||||
# ── Identity provider — Google Workspace ─────────────────────────────────────
|
||||
oidc:
|
||||
issuer: https://accounts.google.com
|
||||
client_id: REPLACE_ME # Google OAuth client ID (not secret; from Cloud Console)
|
||||
client_secret: ${OIDC_CLIENT_SECRET}
|
||||
allowed_email_domains: [REPLACE_ME] # e.g. [example.com] — reject id_tokens outside your org
|
||||
# Google ignores the default offline_access scope; these two are what actually
|
||||
# yield refresh tokens (silent renewal + the deprovision leash) from Google.
|
||||
scopes: [openid, profile, email]
|
||||
extra_auth_params: { access_type: offline, prompt: consent }
|
||||
# NOTE: Google id_tokens carry NO groups claim. For group-based RBAC with
|
||||
# Google as IdP, set `google_groups` (below) and the gateway fetches each
|
||||
# user's Workspace groups at login via the Admin SDK Directory API.
|
||||
# Otherwise, use email_domain matching (see managed.policies below).
|
||||
# google_groups:
|
||||
# service_account_json_path: /secrets/google-sa.json # SA with domain-wide delegation on admin.directory.group.readonly
|
||||
# admin_email: admin@example.com # a Workspace admin the SA impersonates
|
||||
# groups_claim: groups # Okta=groups, Entra app roles=roles — NOT Google
|
||||
# ca_cert_pem: ${file:/secrets/idp-ca.pem} # only for an IdP behind a private CA
|
||||
|
||||
# ── Sessions ─────────────────────────────────────────────────────────────────
|
||||
session:
|
||||
jwt_secret: ${GATEWAY_JWT_SECRET} # >= 32 bytes; openssl rand -base64 32
|
||||
# Google issues refresh tokens (above), so sessions renew silently and this
|
||||
# mainly bounds deprovision latency. 8 is a sane default; lower toward 1 for
|
||||
# tighter revocation. Array form rotates keys: [new, old] (index 0 signs, all verify).
|
||||
ttl_hours: 8
|
||||
|
||||
# ── Store (REQUIRED — the gateway refuses to boot without it) ─────────────────
|
||||
store:
|
||||
postgres_url: ${GATEWAY_POSTGRES_URL} # private-IP Cloud SQL; built with ?sslmode=require by setup.sh
|
||||
|
||||
# ── Upstreams — Agent Platform ───────────────────────────────────────────────
|
||||
upstreams:
|
||||
- provider: vertex
|
||||
region: us-east5 # a region where the Claude models you need are published in Model Garden
|
||||
project_id: REPLACE_ME # your GCP project ID for Agent Platform access
|
||||
auth: {} # ADC via Cloud Run SA / GKE Workload Identity (preferred — no static keys)
|
||||
# base_url: https://us-east5-aiplatform.p.googleapis.com # Private Service Connect endpoint
|
||||
# Add more upstreams for failover (tried top→bottom on 5xx/timeout/501): a
|
||||
# second region, or an anthropic/bedrock fallback. See
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway.
|
||||
|
||||
# ── Telemetry fan-out (OPTIONAL) ─────────────────────────────────────────────
|
||||
# The CLI sends OTLP/HTTP to the gateway; the gateway fans out, stamping
|
||||
# user.id/user.email/user.groups server-side. On GCP, point at an OpenTelemetry
|
||||
# Collector with the googlecloud exporter (-> Cloud Trace / Managed Prometheus).
|
||||
# Takes effect after the second pass (once public_url is the real URL, not the
|
||||
# placeholder): when forward_to and public_url are both configured the gateway pushes
|
||||
# CLAUDE_CODE_ENABLE_TELEMETRY and the OTEL exporter selectors to every client
|
||||
# automatically — no per-developer config needed.
|
||||
# telemetry:
|
||||
# forward_to:
|
||||
# - url: https://otel-collector.internal.example.com:4318
|
||||
# headers:
|
||||
# Authorization: ${file:/secrets/otlp-token}
|
||||
# metrics: true # safe aggregate counters (default)
|
||||
# logs: false # carries bash commands / tool inputs — opt in deliberately
|
||||
# traces: false
|
||||
|
||||
# ── RBAC + managed settings (OPTIONAL; first-match-wins, top -> bottom) ───────
|
||||
# With Google as IdP, match on email_domain, or on group email addresses
|
||||
# (e.g. eng@example.com) once oidc.google_groups is configured above.
|
||||
# managed:
|
||||
# policies:
|
||||
# - match: { email_domain: example.com }
|
||||
# cli:
|
||||
# availableModels: [claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5]
|
||||
# permissions: { deny: ["Read(./.env)", "Read(./secrets/**)"] }
|
||||
# - match: {} # catch-all floor — keep LAST
|
||||
# cli:
|
||||
# availableModels: [claude-sonnet-4-6, claude-haiku-4-5]
|
||||
|
||||
# ── Admin API (OPTIONAL — enables db-mode runtime config + spend caps) ───────
|
||||
# admin_groups needs a groups claim — with Google as IdP, set
|
||||
# oidc.google_groups (above) so Workspace group email addresses populate the
|
||||
# claim, or use the bootstrap keys below instead. Named keys for
|
||||
# attribution in the audit log; 32-char minimum on key values. On Cloud Run add
|
||||
# these as env vars to --set-secrets (or terraform env value_source blocks),
|
||||
# same as the JWT/OIDC/Postgres secrets above; on GKE you may use ${file:...}.
|
||||
# admin:
|
||||
# write_keys:
|
||||
# - id: terraform
|
||||
# key: ${GATEWAY_ADMIN_WRITE_KEY}
|
||||
# read_keys:
|
||||
# - id: reporting
|
||||
# key: ${GATEWAY_ADMIN_READ_KEY}
|
||||
# # admin_groups: [platform-finops@example.com] # group emails via oidc.google_groups, or any groups-capable IdP
|
||||
|
||||
# ── Model catalog (OPTIONAL) ─────────────────────────────────────────────────
|
||||
# Default true: every built-in Claude model is exposed and auto-translated per
|
||||
# upstream. Set false + a models: list to pin IDs (e.g. provisioned throughput).
|
||||
# auto_include_builtin_models: true
|
||||
# models:
|
||||
# - id: claude-opus-4-8
|
||||
# label: Claude Opus 4.8
|
||||
# upstream_model: { vertex: claude-opus-4-8 }
|
||||
Executable
+558
@@ -0,0 +1,558 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# setup.sh — GCP setup for Claude Gateway (walkthrough §1–7b).
|
||||
#
|
||||
# Provisions, in doc order: APIs (§1), service account + IAM (§2), the gateway
|
||||
# container image in Artifact Registry (§3), a Cloud SQL (PostgreSQL) backend
|
||||
# with PRIVATE IP only (§4), the JWT + postgres-url secrets (§5), the
|
||||
# gateway.yaml config secret (§6), and a Cloud Run deploy with Direct VPC
|
||||
# egress (§7b).
|
||||
#
|
||||
# Private IP is required because public IP is disallowed by the org-policy constraint
|
||||
# `constraints/sql.restrictPublicIp`. A Cloud SQL private IP is an address inside a VPC,
|
||||
# so §4 here also provisions the prerequisite VPC + Private Services Access — the
|
||||
# one-time, irreducible networking required for private IP.
|
||||
#
|
||||
# Section markers (§N) below map to the walkthrough:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-on-gcp
|
||||
#
|
||||
# Covers here: APIs (§1) -> service account + IAM (§2) -> build & push image (§3)
|
||||
# -> VPC + Private Services Access -> Cloud SQL (private IP only) -> database
|
||||
# + user (§4) -> jwt + postgres-url secrets (§5) -> gateway-config
|
||||
# secret from gateway.yaml (§6) -> Cloud Run deploy (§7b).
|
||||
# Not covered: GKE track (§7a) — Cloud Run is the lower-friction path here.
|
||||
#
|
||||
# Idempotent: existing resources are detected and skipped, so it is safe to re-run.
|
||||
# Override any default below via environment variable, e.g. `REGION=us-east5 ./setup.sh`.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- configuration (env-overridable) ----------------------------------------
|
||||
PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project 2>/dev/null)}"
|
||||
REGION="${REGION:-${CLOUDSDK_COMPUTE_REGION:-us-east5}}" # guide §1 uses us-east5 (Agent Platform model region)
|
||||
|
||||
SA_NAME="${SA_NAME:-claude-gateway}" # §2 service account
|
||||
SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
|
||||
|
||||
# §3 image
|
||||
AR_REPO="${AR_REPO:-claude-gateway}" # Artifact Registry repository
|
||||
IMAGE_NAME="${IMAGE_NAME:-gateway}"
|
||||
RELEASES_URL="${RELEASES_URL:-https://downloads.claude.ai/claude-code-releases}" # public Claude Code release endpoint
|
||||
VERSION="${VERSION:-}" # Claude Code release to deploy; empty = latest release (resolved below)
|
||||
VERSION_FILE="${VERSION_FILE:-./.claude-version}" # pins the resolved release across re-runs; delete it (or set VERSION) to upgrade
|
||||
DOCKERFILE="${DOCKERFILE:-./Dockerfile}"
|
||||
CLAUDE_BINARY="${CLAUDE_BINARY:-./claude}" # linux-x64 Claude Code binary; downloaded from RELEASES_URL if missing
|
||||
CLAUDE_SHA256="${CLAUDE_SHA256:-}" # optional: out-of-band sha256 pin for the downloaded binary, checked in addition to the release manifest
|
||||
|
||||
VPC_NETWORK="${VPC_NETWORK:-cc-gateway-vpc}"
|
||||
SUBNET="${SUBNET:-cc-gateway-subnet}"
|
||||
SUBNET_RANGE="${SUBNET_RANGE:-10.0.0.0/24}"
|
||||
|
||||
PSA_RANGE_NAME="${PSA_RANGE_NAME:-google-managed-services-${VPC_NETWORK}}"
|
||||
PSA_PREFIX_LENGTH="${PSA_PREFIX_LENGTH:-16}" # /16 is GCP's recommendation; reserved, not consumed
|
||||
|
||||
DB_INSTANCE="${DB_INSTANCE:-claude-gateway-db}"
|
||||
DB_VERSION="${DB_VERSION:-POSTGRES_16}" # PG14+ supported; 16 is the recommended default (§4)
|
||||
DB_TIER="${DB_TIER:-db-g1-small}"
|
||||
DB_NAME="${DB_NAME:-claude_gateway}"
|
||||
DB_USER="${DB_USER:-gateway}"
|
||||
|
||||
SECRET_NAME="${SECRET_NAME:-gateway-postgres-url}" # §5 store.postgres_url
|
||||
JWT_SECRET_NAME="${JWT_SECRET_NAME:-gateway-jwt-secret}" # §5 session.jwt_secret
|
||||
|
||||
GATEWAY_YAML="${GATEWAY_YAML:-./gateway.yaml}" # §6 config file
|
||||
CONFIG_SECRET="${CONFIG_SECRET:-gateway-config}" # §6 mounted at /etc/claude/gateway.yaml
|
||||
|
||||
# §7 Cloud Run deploy
|
||||
SERVICE_NAME="${SERVICE_NAME:-claude-gateway}"
|
||||
OIDC_SECRET_NAME="${OIDC_SECRET_NAME:-gateway-oidc-client-secret}" # operator-created (Google OAuth client)
|
||||
DEPLOY="${DEPLOY:-1}" # set DEPLOY=0 to provision only, no Cloud Run deploy
|
||||
INGRESS="${INGRESS:-internal}" # internal (default; no public URL) | internal-and-cloud-load-balancing (only if you front it with your own internal ALB)
|
||||
MAX_INSTANCES="${MAX_INSTANCES:-8}" # keep MAX_INSTANCES × store.max_connections (default 5) below the DB tier's max_connections (~50 on db-g1-small); raise the tier before raising this
|
||||
|
||||
# ---- helpers ----------------------------------------------------------------
|
||||
log() { printf '\n==> %s\n' "$*"; }
|
||||
skip() { printf ' (exists) %s\n' "$*"; }
|
||||
curl_https() { curl --proto '=https' --proto-redir '=https' --tlsv1.2 "$@"; } # refuse plaintext/protocol-downgrade
|
||||
sha_of() { openssl dgst -sha256 "$1" | awk '{print $NF}'; } # openssl avoids shasum/sha256sum portability gaps
|
||||
|
||||
if [[ -z "${PROJECT_ID}" ]]; then
|
||||
echo "ERROR: PROJECT_ID is not set and no gcloud default project is configured." >&2
|
||||
echo " Set it with: export PROJECT_ID=<your-project> (or 'gcloud config set project ...')" >&2
|
||||
exit 1
|
||||
fi
|
||||
# VERSION tags the image and selects the public Claude Code release to download.
|
||||
# The first resolved value is pinned to ${VERSION_FILE} so the documented
|
||||
# re-runs (fill gateway.yaml -> re-run; set public_url -> re-run) don't silently
|
||||
# build and deploy a newer release mid-bootstrap.
|
||||
if [[ -z "${VERSION}" && -f "${VERSION_FILE}" ]]; then
|
||||
VERSION="$(< "${VERSION_FILE}")"
|
||||
log "Using release pinned in ${VERSION_FILE}: ${VERSION} (delete the file or set VERSION to change it)"
|
||||
elif [[ -z "${VERSION}" ]]; then
|
||||
# /latest is the channel the official installer (claude.ai/install.sh) uses.
|
||||
VERSION="$(curl_https -fsSL "${RELEASES_URL}/latest" | tr -d '[:space:]' || true)"
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
echo "ERROR: could not resolve the latest release from ${RELEASES_URL}/latest." >&2
|
||||
echo " Set VERSION to a Claude Code release version, e.g. export VERSION=2.1.195" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "VERSION not set — using latest Claude Code release: ${VERSION}"
|
||||
fi
|
||||
# Reject non-version content (e.g. an HTML error page served with HTTP 200)
|
||||
# before it reaches the image tag and download URLs.
|
||||
if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
echo "ERROR: '${VERSION}' is not a release version (from VERSION, ${VERSION_FILE}, or ${RELEASES_URL}/latest)." >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "${VERSION}" > "${VERSION_FILE}"
|
||||
IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO}/${IMAGE_NAME}:${VERSION}"
|
||||
# Claude Code only connects to a gateway whose hostname resolves to private
|
||||
# addresses (a client-side /login check), so public ingress can never serve
|
||||
# clients — mirror the terraform module's validation and refuse it up front.
|
||||
if [[ "${INGRESS}" != "internal" && "${INGRESS}" != "internal-and-cloud-load-balancing" ]]; then
|
||||
echo "ERROR: INGRESS must be 'internal' or 'internal-and-cloud-load-balancing' — Claude Code's" >&2
|
||||
echo " /login only accepts gateway hosts on private addresses, so public ingress cannot serve clients." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Project: ${PROJECT_ID} Region: ${REGION} VPC: ${VPC_NETWORK}"
|
||||
|
||||
# ---- 1 Project & API setup ------------------------------------------------
|
||||
# walkthrough §1 list (aiplatform, artifactregistry, sqladmin, secretmanager, iamcredentials)
|
||||
# plus iam/compute/servicenetworking required for the SA + private-IP networking below.
|
||||
# container.googleapis.com is for the GKE track (§7a) — harmless if you stay on Cloud Run.
|
||||
# We pass --project on every call rather than mutating your gcloud config.
|
||||
log "Enabling required APIs (§1)"
|
||||
gcloud services enable \
|
||||
aiplatform.googleapis.com \
|
||||
artifactregistry.googleapis.com \
|
||||
sqladmin.googleapis.com \
|
||||
secretmanager.googleapis.com \
|
||||
iamcredentials.googleapis.com \
|
||||
iam.googleapis.com \
|
||||
compute.googleapis.com \
|
||||
container.googleapis.com \
|
||||
servicenetworking.googleapis.com \
|
||||
run.googleapis.com \
|
||||
--project="${PROJECT_ID}"
|
||||
|
||||
# ---- 2 Service account & IAM ----------------------------------------------
|
||||
log "Creating service account ${SA_EMAIL} and granting project roles (§2)"
|
||||
if gcloud iam service-accounts describe "${SA_EMAIL}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "service account ${SA_EMAIL}"
|
||||
else
|
||||
gcloud iam service-accounts create "${SA_NAME}" \
|
||||
--display-name="Claude Gateway" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# add-iam-policy-binding is idempotent (re-adding an existing binding is a no-op).
|
||||
# --condition=None avoids the interactive condition prompt in non-interactive runs.
|
||||
#
|
||||
# Only aiplatform.user is granted: the gateway reaches Cloud SQL over the VPC at
|
||||
# its PRIVATE IP with a password user (§4/§7b — direct TCP, not the Cloud SQL
|
||||
# Auth Proxy / connector), so it never calls cloudsql.instances.connect and no
|
||||
# roles/cloudsql.client grant is needed. Direct private-IP is used because the
|
||||
# gateway's store is a plain postgres_url — no proxy sidecar/socket plumbing,
|
||||
# one less moving part, and the connection string is portable across Cloud Run
|
||||
# and GKE.
|
||||
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/aiplatform.user" --condition=None >/dev/null # Agent Platform inference (§2)
|
||||
|
||||
# ---- 3 Build & push image to Artifact Registry ----------------------------
|
||||
log "Ensuring Artifact Registry repo and image (§3)"
|
||||
if gcloud artifacts repositories describe "${AR_REPO}" \
|
||||
--location="${REGION}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "Artifact Registry repo ${AR_REPO}"
|
||||
else
|
||||
gcloud artifacts repositories create "${AR_REPO}" \
|
||||
--repository-format=docker --location="${REGION}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# Image is the expensive, already-done step: skip the build+push entirely if the
|
||||
# tag already exists in the registry.
|
||||
if gcloud artifacts docker images describe "${IMAGE}" >/dev/null 2>&1; then
|
||||
skip "image ${IMAGE}"
|
||||
else
|
||||
# The public Claude Code release includes the gateway subcommand, so the
|
||||
# binary comes straight from the release endpoint, verified against the
|
||||
# release manifest's sha256. A pre-existing ${CLAUDE_BINARY} (stale version,
|
||||
# interrupted download, hand-placed file) is verified the same way and
|
||||
# re-downloaded on mismatch, so an unverified binary can never reach the image.
|
||||
manifest="$(curl_https -fsSL "${RELEASES_URL}/${VERSION}/manifest.json" | tr -d '[:space:]' || true)"
|
||||
sha_re='"linux-x64"[^}]*"checksum":"([a-f0-9]{64})"' # structure-based: survives pretty-printed, minified, and one-line-per-platform manifests
|
||||
if [[ ! "${manifest}" =~ ${sha_re} ]]; then
|
||||
echo "ERROR: could not read the linux-x64 sha256 from ${RELEASES_URL}/${VERSION}/manifest.json — refusing to build." >&2
|
||||
exit 1
|
||||
fi
|
||||
expected_sha="${BASH_REMATCH[1]}"
|
||||
if [[ -f "${CLAUDE_BINARY}" && "$(sha_of "${CLAUDE_BINARY}")" == "${expected_sha}" ]]; then
|
||||
skip "binary ${CLAUDE_BINARY} (sha256 matches release ${VERSION})"
|
||||
else
|
||||
if [[ -f "${CLAUDE_BINARY}" ]]; then
|
||||
log "Existing ${CLAUDE_BINARY} does not match release ${VERSION} — re-downloading"
|
||||
else
|
||||
log "Downloading Claude Code ${VERSION} (linux-x64) from ${RELEASES_URL}"
|
||||
fi
|
||||
# Until verification passes, ANY exit (curl failure, set -e, signal, the
|
||||
# error exit below) removes the file, so a partial download can't be
|
||||
# silently picked up by a later run.
|
||||
trap 'rm -f "${CLAUDE_BINARY}"' EXIT INT TERM
|
||||
curl_https -fL -o "${CLAUDE_BINARY}" "${RELEASES_URL}/${VERSION}/linux-x64/claude"
|
||||
actual_sha="$(sha_of "${CLAUDE_BINARY}")"
|
||||
if [[ "${actual_sha}" != "${expected_sha}" ]]; then
|
||||
echo "ERROR: sha256 of ${CLAUDE_BINARY} is ${actual_sha} but the release manifest says ${expected_sha} — refusing to build." >&2
|
||||
exit 1
|
||||
fi
|
||||
trap - EXIT INT TERM
|
||||
log "Verified binary sha256 ${actual_sha}"
|
||||
fi
|
||||
# Optional out-of-band pin, checked even for a pre-existing binary: the
|
||||
# manifest shares an origin with the binary, so it can't defend against a
|
||||
# compromised endpoint — CLAUDE_SHA256 can.
|
||||
if [[ -n "${CLAUDE_SHA256}" && "$(sha_of "${CLAUDE_BINARY}")" != "${CLAUDE_SHA256}" ]]; then
|
||||
echo "ERROR: sha256 of ${CLAUDE_BINARY} does not match CLAUDE_SHA256 (${CLAUDE_SHA256}) — refusing to build." >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "${CLAUDE_BINARY}"
|
||||
log "Building and pushing ${IMAGE}"
|
||||
gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
|
||||
# Cloud Run requires linux/amd64. --platform forces it (e.g. when building on an
|
||||
# Apple Silicon Mac), and --provenance=false keeps buildx from wrapping the result
|
||||
# in an OCI image index that Cloud Run rejects ("manifest ... must support amd64/linux").
|
||||
docker build --platform=linux/amd64 --provenance=false \
|
||||
-f "${DOCKERFILE}" --build-arg CLAUDE_BINARY="${CLAUDE_BINARY}" -t "${IMAGE}" .
|
||||
docker push "${IMAGE}"
|
||||
fi
|
||||
|
||||
# ---- 4 VPC + Private Services Access (private-IP prerequisite) -------------
|
||||
log "Creating VPC network and subnet"
|
||||
if gcloud compute networks describe "${VPC_NETWORK}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "network ${VPC_NETWORK}"
|
||||
else
|
||||
gcloud compute networks create "${VPC_NETWORK}" \
|
||||
--subnet-mode=custom --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
if gcloud compute networks subnets describe "${SUBNET}" \
|
||||
--region="${REGION}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "subnet ${SUBNET}"
|
||||
else
|
||||
gcloud compute networks subnets create "${SUBNET}" \
|
||||
--network="${VPC_NETWORK}" --region="${REGION}" \
|
||||
--range="${SUBNET_RANGE}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
log "Configuring Private Services Access (allocated range + VPC peering)"
|
||||
if gcloud compute addresses describe "${PSA_RANGE_NAME}" \
|
||||
--global --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "allocated range ${PSA_RANGE_NAME}"
|
||||
else
|
||||
gcloud compute addresses create "${PSA_RANGE_NAME}" \
|
||||
--global --purpose=VPC_PEERING --prefix-length="${PSA_PREFIX_LENGTH}" \
|
||||
--network="${VPC_NETWORK}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
if gcloud services vpc-peerings list --network="${VPC_NETWORK}" --project="${PROJECT_ID}" \
|
||||
--format='value(peering)' 2>/dev/null | grep -q servicenetworking; then
|
||||
skip "servicenetworking VPC peering"
|
||||
else
|
||||
gcloud services vpc-peerings connect \
|
||||
--service=servicenetworking.googleapis.com \
|
||||
--ranges="${PSA_RANGE_NAME}" \
|
||||
--network="${VPC_NETWORK}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# ---- 4 Cloud SQL instance (private IP only) -------------------------------
|
||||
log "Creating Cloud SQL instance ${DB_INSTANCE} (private IP only)"
|
||||
if gcloud sql instances describe "${DB_INSTANCE}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "instance ${DB_INSTANCE}"
|
||||
else
|
||||
gcloud sql instances create "${DB_INSTANCE}" \
|
||||
--database-version="${DB_VERSION}" \
|
||||
--tier="${DB_TIER}" \
|
||||
--region="${REGION}" \
|
||||
--network="projects/${PROJECT_ID}/global/networks/${VPC_NETWORK}" \
|
||||
--no-assign-ip \
|
||||
--project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
log "Creating database ${DB_NAME}"
|
||||
if gcloud sql databases describe "${DB_NAME}" \
|
||||
--instance="${DB_INSTANCE}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "database ${DB_NAME}"
|
||||
else
|
||||
gcloud sql databases create "${DB_NAME}" \
|
||||
--instance="${DB_INSTANCE}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# hex (not base64) keeps the password URL-safe for the connection string below.
|
||||
log "Creating database user ${DB_USER}"
|
||||
DB_PASSWORD=""
|
||||
if gcloud sql users list --instance="${DB_INSTANCE}" --project="${PROJECT_ID}" \
|
||||
--format='value(name)' 2>/dev/null | grep -qx "${DB_USER}"; then
|
||||
if gcloud secrets describe "${SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "user ${DB_USER} (password unchanged; secret not rewritten)"
|
||||
else
|
||||
# Self-heal: a previous run died after creating the user but before writing
|
||||
# the connection-string secret, losing the only copy of the password. The
|
||||
# secret is the password's only consumer, so resetting it is safe and keeps
|
||||
# re-runs able to recover from any partial state.
|
||||
log "User ${DB_USER} exists but secret ${SECRET_NAME} is missing — resetting password"
|
||||
DB_PASSWORD="$(openssl rand -hex 24)"
|
||||
gcloud sql users set-password "${DB_USER}" \
|
||||
--instance="${DB_INSTANCE}" --password="${DB_PASSWORD}" \
|
||||
--project="${PROJECT_ID}"
|
||||
fi
|
||||
else
|
||||
DB_PASSWORD="$(openssl rand -hex 24)"
|
||||
gcloud sql users create "${DB_USER}" \
|
||||
--instance="${DB_INSTANCE}" --password="${DB_PASSWORD}" \
|
||||
--project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# ---- 5 Connection string -> Secret Manager + secretAccessor ---------------
|
||||
PRIVATE_IP="$(gcloud sql instances describe "${DB_INSTANCE}" --project="${PROJECT_ID}" \
|
||||
--format='value(ipAddresses[0].ipAddress)')"
|
||||
|
||||
if [[ -n "${DB_PASSWORD}" ]]; then
|
||||
# direct private-IP form, ?sslmode=require (guide §4)
|
||||
CONN="postgres://${DB_USER}:${DB_PASSWORD}@${PRIVATE_IP}:5432/${DB_NAME}?sslmode=require"
|
||||
log "Storing connection string in Secret Manager secret ${SECRET_NAME}"
|
||||
if gcloud secrets describe "${SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
printf '%s' "${CONN}" | gcloud secrets versions add "${SECRET_NAME}" \
|
||||
--data-file=- --project="${PROJECT_ID}"
|
||||
else
|
||||
printf '%s' "${CONN}" | gcloud secrets create "${SECRET_NAME}" \
|
||||
--replication-policy=automatic --data-file=- --project="${PROJECT_ID}"
|
||||
fi
|
||||
else
|
||||
log "Skipping secret write (user already existed, password not available this run)"
|
||||
fi
|
||||
|
||||
if gcloud secrets describe "${SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
log "Granting ${SA_EMAIL} secretAccessor on ${SECRET_NAME}"
|
||||
gcloud secrets add-iam-policy-binding "${SECRET_NAME}" \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--condition=None --project="${PROJECT_ID}" >/dev/null
|
||||
fi
|
||||
|
||||
# JWT signing secret — generated once (re-runs do NOT rotate it).
|
||||
log "Ensuring JWT signing secret ${JWT_SECRET_NAME} (§5)"
|
||||
if gcloud secrets describe "${JWT_SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "secret ${JWT_SECRET_NAME}"
|
||||
else
|
||||
openssl rand -base64 32 | tr -d '\n' | gcloud secrets create "${JWT_SECRET_NAME}" \
|
||||
--replication-policy=automatic --data-file=- --project="${PROJECT_ID}"
|
||||
fi
|
||||
gcloud secrets add-iam-policy-binding "${JWT_SECRET_NAME}" \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--condition=None --project="${PROJECT_ID}" >/dev/null
|
||||
|
||||
# OIDC client secret — operator-created (the script can't generate it). Grant
|
||||
# accessor here once it exists so the deploy step doesn't fail on permission.
|
||||
if gcloud secrets describe "${OIDC_SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
log "Granting ${SA_EMAIL} secretAccessor on ${OIDC_SECRET_NAME}"
|
||||
gcloud secrets add-iam-policy-binding "${OIDC_SECRET_NAME}" \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--condition=None --project="${PROJECT_ID}" >/dev/null
|
||||
fi
|
||||
|
||||
# ---- 6 gateway.yaml -> Secret Manager (gateway-config) --------------------
|
||||
# Published only when fully filled in: refuse to push a config that still has
|
||||
# REPLACE_ME placeholders (checked on non-comment lines so commented examples
|
||||
# and this file's header don't trip the guard).
|
||||
log "Publishing ${GATEWAY_YAML} as Secret Manager secret ${CONFIG_SECRET} (§6)"
|
||||
if [[ ! -f "${GATEWAY_YAML}" ]]; then
|
||||
echo " (skip) ${GATEWAY_YAML} not found — run 'cp gateway.yaml.example gateway.yaml', fill it in, then re-run (§6)."
|
||||
elif grep -vE '^[[:space:]]*#' "${GATEWAY_YAML}" | grep -q 'REPLACE_ME'; then
|
||||
echo " (skip) ${GATEWAY_YAML} still has REPLACE_ME placeholders to fill:"
|
||||
grep -nE 'REPLACE_ME' "${GATEWAY_YAML}" | grep -vE '^[0-9]+:[[:space:]]*#' | sed 's/^/ /'
|
||||
echo " Fill them in, then re-run to publish ${CONFIG_SECRET}."
|
||||
else
|
||||
if gcloud secrets describe "${CONFIG_SECRET}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
gcloud secrets versions add "${CONFIG_SECRET}" \
|
||||
--data-file="${GATEWAY_YAML}" --project="${PROJECT_ID}"
|
||||
else
|
||||
gcloud secrets create "${CONFIG_SECRET}" --replication-policy=automatic \
|
||||
--data-file="${GATEWAY_YAML}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
gcloud secrets add-iam-policy-binding "${CONFIG_SECRET}" \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--condition=None --project="${PROJECT_ID}" >/dev/null
|
||||
fi
|
||||
|
||||
# ---- 7 Cloud Run deploy (Direct VPC egress) -------------------------------
|
||||
# Direct VPC egress (--network/--subnet/--vpc-egress) puts the service on the
|
||||
# VPC so it reaches the Cloud SQL PRIVATE IP directly — matching the private-IP
|
||||
# connection string in the postgres-url secret. private-ranges-only keeps public
|
||||
# egress (Agent Platform, accounts.google.com) off the VPC, so no Cloud NAT is needed.
|
||||
# We deliberately do NOT use --add-cloudsql-instances (that's the Auth Proxy /
|
||||
# socket path, which would need a different connection string).
|
||||
#
|
||||
# Secrets: gateway.yaml is mounted as a FILE at /etc/claude (alone in its dir).
|
||||
# The JWT / OIDC / Postgres secrets are injected as ENV VARS — Cloud Run cannot
|
||||
# mount multiple secrets into one directory, and gateway.yaml references them via
|
||||
# ${ENV_VAR}. (See the env-var names in gateway.yaml: GATEWAY_JWT_SECRET etc.)
|
||||
#
|
||||
# Self-gating: deploy only once its inputs exist (config secret published + the
|
||||
# operator-provided OIDC client secret). On a first run these are missing and it
|
||||
# cleanly skips.
|
||||
RUN_URL=""
|
||||
missing=""
|
||||
gcloud secrets describe "${CONFIG_SECRET}" --project="${PROJECT_ID}" >/dev/null 2>&1 || missing="${missing} ${CONFIG_SECRET}"
|
||||
gcloud secrets describe "${OIDC_SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1 || missing="${missing} ${OIDC_SECRET_NAME}"
|
||||
# Also gate on the postgres-url secret (referenced by --set-secrets below): if it
|
||||
# is somehow absent, skip with a clear message rather than failing the deploy with
|
||||
# a raw Cloud Run missing-secret error.
|
||||
gcloud secrets describe "${SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1 || missing="${missing} ${SECRET_NAME}"
|
||||
|
||||
if [[ "${DEPLOY}" != "1" ]]; then
|
||||
log "Skipping Cloud Run deploy (DEPLOY=${DEPLOY}) (§7)"
|
||||
elif [[ -n "${missing// }" ]]; then
|
||||
log "Skipping Cloud Run deploy — missing secret(s):${missing} (§7)"
|
||||
echo " Fill ${GATEWAY_YAML} and re-run to publish ${CONFIG_SECRET}; create ${OIDC_SECRET_NAME}"
|
||||
echo " from the Google OAuth client. Then re-run to deploy."
|
||||
else
|
||||
SECRET_MOUNTS="/etc/claude/gateway.yaml=${CONFIG_SECRET}:latest" # file mount (alone in /etc/claude)
|
||||
SECRET_MOUNTS="${SECRET_MOUNTS},GATEWAY_JWT_SECRET=${JWT_SECRET_NAME}:latest" # env var
|
||||
SECRET_MOUNTS="${SECRET_MOUNTS},OIDC_CLIENT_SECRET=${OIDC_SECRET_NAME}:latest" # env var
|
||||
SECRET_MOUNTS="${SECRET_MOUNTS},GATEWAY_POSTGRES_URL=${SECRET_NAME}:latest" # env var
|
||||
|
||||
log "Deploying Cloud Run service ${SERVICE_NAME} (§7b, Direct VPC egress)"
|
||||
# Deploy private (--no-allow-unauthenticated avoids the interactive prompt and
|
||||
# keeps allUsers OUT of the deploy, so a Domain-Restricted-Sharing org doesn't
|
||||
# fail the deploy on the IAM step). Public access is attempted separately below.
|
||||
#
|
||||
# --ingress is passed EXPLICITLY because it is sticky across redeploys (omitting
|
||||
# it keeps the previous value). The default, internal, keeps the *.run.app URL
|
||||
# off the public internet — reachable only from this VPC, or from corp networks
|
||||
# with the PSC endpoint + private run.app DNS plumbing (see terraform/README.md
|
||||
# "Private access"). Public ingress cannot serve clients (see the INGRESS
|
||||
# guard at the top of this script), so the two-pass OAuth bootstrap has to be
|
||||
# completed from inside the VPC (or a PSC-connected corp network). Use
|
||||
# internal-and-cloud-load-balancing instead if you front the service with
|
||||
# your own internal ALB.
|
||||
#
|
||||
# --timeout=3600 raises Cloud Run's default 300s request timeout, which would
|
||||
# otherwise cut off long streaming /v1/messages responses mid-stream.
|
||||
#
|
||||
# --max-instances bounds the Postgres connection footprint: each instance
|
||||
# opens a pool of up to 5 connections (store.max_connections default) and
|
||||
# db-g1-small caps at ~50 max_connections, so the default ceiling of 100
|
||||
# instances would crash-loop new instances under load. Keep
|
||||
# max-instances × 5 below the DB tier's max_connections; raise the DB tier
|
||||
# (or set store.max_connections lower) before raising this.
|
||||
gcloud run deploy "${SERVICE_NAME}" \
|
||||
--image="${IMAGE}" \
|
||||
--region="${REGION}" \
|
||||
--service-account="${SA_EMAIL}" \
|
||||
--min-instances=1 \
|
||||
--max-instances="${MAX_INSTANCES}" \
|
||||
--port=8080 \
|
||||
--timeout=3600 \
|
||||
--ingress="${INGRESS}" \
|
||||
--network="${VPC_NETWORK}" \
|
||||
--subnet="${SUBNET}" \
|
||||
--vpc-egress=private-ranges-only \
|
||||
--set-secrets="${SECRET_MOUNTS}" \
|
||||
--no-allow-unauthenticated \
|
||||
--project="${PROJECT_ID}"
|
||||
|
||||
# The gateway runs its OWN OIDC, so the Cloud Run IAM layer must allow
|
||||
# unauthenticated. Attempt it separately and tolerate failure: Domain Restricted
|
||||
# Sharing (iam.allowedPolicyMemberDomains) blocks allUsers in hardened orgs.
|
||||
log "Granting public invoker (allUsers) — required for the gateway's OIDC login"
|
||||
if gcloud run services add-iam-policy-binding "${SERVICE_NAME}" \
|
||||
--region="${REGION}" --member=allUsers --role=roles/run.invoker \
|
||||
--project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
echo " public invoker granted."
|
||||
else
|
||||
echo " WARN: allUsers rejected (likely Domain Restricted Sharing). The service is"
|
||||
echo " deployed but the invoker IAM check is still enabled, so requests 403"
|
||||
echo " before reaching the container. Preferred fix (where available):"
|
||||
echo " gcloud run services update ${SERVICE_NAME} --no-invoker-iam-check \\"
|
||||
echo " --region=${REGION} --project=${PROJECT_ID}"
|
||||
echo " Alternatively: request a DRS exception for ${SERVICE_NAME}, or use the GKE"
|
||||
echo " track, which exposes the gateway at the network layer with no allUsers"
|
||||
echo " binding. An LB is NOT a fix — it does not bypass the invoker IAM check."
|
||||
fi
|
||||
|
||||
RUN_URL="$(gcloud run services describe "${SERVICE_NAME}" --region="${REGION}" \
|
||||
--project="${PROJECT_ID}" --format='value(status.url)')"
|
||||
log "Cloud Run URL: ${RUN_URL}"
|
||||
|
||||
# public_url is now required (config validation refuses a non-loopback bind
|
||||
# without it), so the template ships a placeholder for the first pass. Once we
|
||||
# know the real URL, warn on any mismatch so the operator doesn't leave the
|
||||
# placeholder — or a stale hostname — in place. Normalize quotes / inline
|
||||
# comments / a trailing slash so schema-equivalent spellings compare equal.
|
||||
# Only checked with internal ingress, where public_url should be the run.app
|
||||
# URL; behind an internal ALB it is the ALB hostname, which this script
|
||||
# cannot know.
|
||||
CFG_PUBLIC_URL="$(grep -E '^[[:space:]]*public_url:' "${GATEWAY_YAML}" 2>/dev/null \
|
||||
| head -1 \
|
||||
| sed -E 's/^[[:space:]]*public_url:[[:space:]]*//; s/[[:space:]]+#.*$//; s/[[:space:]]*$//' \
|
||||
|| true)"
|
||||
CFG_PUBLIC_URL="${CFG_PUBLIC_URL#[\'\"]}"; CFG_PUBLIC_URL="${CFG_PUBLIC_URL%[\'\"]}"
|
||||
CFG_PUBLIC_URL="${CFG_PUBLIC_URL%/}"
|
||||
if [[ "${INGRESS}" == "internal" && -n "${RUN_URL}" && "${CFG_PUBLIC_URL}" != "${RUN_URL%/}" ]]; then
|
||||
echo " NOTE — ${GATEWAY_YAML} has public_url: ${CFG_PUBLIC_URL:-<unset>}"
|
||||
echo " but this service's URL is ${RUN_URL}."
|
||||
echo " Set listen.public_url to ${RUN_URL} (or your LB hostname) and re-run."
|
||||
fi
|
||||
|
||||
if [[ -n "${RUN_URL}" ]]; then
|
||||
# gcloud run deploy already fails the script if the revision can't boot (it
|
||||
# waits for the Ready condition), so what's left to verify is that the
|
||||
# gateway is serving. The OAuth discovery document below returns 200 only
|
||||
# after config load, OIDC discovery, upstream construction, and Postgres
|
||||
# migration all succeed, so it doubles as an end-to-end boot check (the
|
||||
# readiness probe proper is GET /readyz). With internal ingress the URL is
|
||||
# reachable only from inside the VPC (or a PSC-connected corp network), so
|
||||
# verification is left to the operator rather than attempted from here.
|
||||
log "Verify the gateway is serving (from inside the VPC, or a PSC-connected corp network):"
|
||||
echo " curl -s ${RUN_URL}/.well-known/oauth-authorization-server"
|
||||
echo " If it isn't responding yet, check logs:"
|
||||
echo " gcloud run services logs read ${SERVICE_NAME} --region=${REGION} --project=${PROJECT_ID}"
|
||||
|
||||
log "Finish the OAuth bootstrap:"
|
||||
echo " 1. Register this redirect URI on the Google OAuth client: ${RUN_URL}/oauth/callback"
|
||||
echo " 2. Set listen.public_url in ${GATEWAY_YAML} to ${RUN_URL}, then re-run: INGRESS=${INGRESS} ./setup.sh"
|
||||
echo " (republishes ${CONFIG_SECRET} and redeploys so the IdP redirect_uri matches)."
|
||||
echo " With INGRESS=internal-and-cloud-load-balancing, use your internal ALB hostname"
|
||||
echo " instead of the run.app URL in both steps."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- summary ----------------------------------------------------------------
|
||||
cat <<EOF
|
||||
|
||||
==> Done.
|
||||
|
||||
Service account ${SA_EMAIL}
|
||||
roles: aiplatform.user, secretmanager.secretAccessor
|
||||
Image ${IMAGE}
|
||||
Instance ${DB_INSTANCE}
|
||||
Connection name ${PROJECT_ID}:${REGION}:${DB_INSTANCE}
|
||||
Private IP ${PRIVATE_IP}
|
||||
Database / user ${DB_NAME} / ${DB_USER}
|
||||
Secrets ${SECRET_NAME}, ${JWT_SECRET_NAME}, ${CONFIG_SECRET}
|
||||
Cloud Run service ${SERVICE_NAME} -> ${RUN_URL:-(not deployed yet)} (ingress: ${INGRESS})
|
||||
|
||||
Next steps (see https://code.claude.com/docs/en/claude-apps-gateway-on-gcp):
|
||||
- Create the one operator-provided secret (from the Google Cloud Console OAuth client):
|
||||
printf '%s' "<client-secret>" | gcloud secrets create ${OIDC_SECRET_NAME} \\
|
||||
--data-file=- --project="${PROJECT_ID}"
|
||||
setup.sh grants ${SA_EMAIL} secretAccessor on it on the next re-run.
|
||||
- Fill in the REPLACE_ME values in ${GATEWAY_YAML}, then re-run: setup.sh publishes
|
||||
${CONFIG_SECRET} and deploys ${SERVICE_NAME} once both secrets exist.
|
||||
- After the first deploy: set listen.public_url to the Cloud Run URL above (or your
|
||||
internal ALB hostname) and register <url>/oauth/callback on the Google OAuth client,
|
||||
then re-run to redeploy.
|
||||
- The gateway runs its own schema migrations at boot, so ${DB_USER} needs CREATE TABLE.
|
||||
EOF
|
||||
@@ -0,0 +1,13 @@
|
||||
# Never commit state (contains secrets) or local var files
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
.terraform/
|
||||
terraform.tfvars
|
||||
*.auto.tfvars
|
||||
crash.log
|
||||
|
||||
# The lock file holds no secrets. It's ignored here so consumers who copy this
|
||||
# example into their own repo generate (and commit) their own platform-complete
|
||||
# lock at first init — committing one from this repo would carry only one
|
||||
# platform's provider hashes.
|
||||
.terraform.lock.hcl
|
||||
@@ -0,0 +1,160 @@
|
||||
# Claude Gateway — Terraform (Cloud Run)
|
||||
|
||||
Terraform equivalent of `../setup.sh`. Lets end-users provision and manage
|
||||
the gateway with `terraform apply`. Covers the same scope ([walkthrough](https://code.claude.com/docs/en/claude-apps-gateway-on-gcp) §1–7): APIs →
|
||||
service account + IAM → Artifact Registry repo → VPC + Private Services Access →
|
||||
private-IP Cloud SQL (PG16) → secrets → Cloud Run with Direct VPC egress.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `versions.tf` | Provider pins (google, random) |
|
||||
| `variables.tf` | All inputs (defaults match `setup.sh`'s) |
|
||||
| `main.tf` | Resources |
|
||||
| `outputs.tf` | Service URL, OAuth redirect URI, SA, DB info |
|
||||
| `terraform.tfvars.example` | Copy to `terraform.tfvars` and edit |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **`../gateway.yaml` created and filled in** — copy the template first:
|
||||
`cp ../gateway.yaml.example ../gateway.yaml`, then replace every `REPLACE_ME`
|
||||
(Terraform reads this file and enforces no `REPLACE_ME` via a precondition).
|
||||
Leave `public_url` at its placeholder for the first apply; set it to the
|
||||
`run.app` URL (the `service_url` output) or your LB hostname and re-apply.
|
||||
`gateway.yaml` is gitignored; the committed template is `gateway.yaml.example`.
|
||||
2. A **remote backend** for shared use (see below). State holds secrets — never commit it.
|
||||
|
||||
## Deploy
|
||||
|
||||
Terraform creates the Artifact Registry repo but does **not** build/push the
|
||||
image, so the apply is two passes: a targeted apply to create the repo, then
|
||||
build/push, then the full apply.
|
||||
|
||||
```bash
|
||||
cp terraform.tfvars.example terraform.tfvars # edit it
|
||||
terraform init
|
||||
|
||||
# 1. Create just the Artifact Registry repo (the -target warning is expected):
|
||||
terraform apply -target=google_artifact_registry_repository.repo
|
||||
|
||||
# 2. Download the public Claude Code linux-x64 release binary (it includes the
|
||||
# `gateway` subcommand; the Dockerfile picks it up at gcp/claude), verify its
|
||||
# sha256 against the release manifest, then build and push the image:
|
||||
BASE="https://downloads.claude.ai/claude-code-releases"
|
||||
VERSION="$(curl -fsSL --proto '=https' "${BASE}/latest")"
|
||||
curl -fL --proto '=https' --proto-redir '=https' -o ../claude \
|
||||
"${BASE}/${VERSION}/linux-x64/claude"
|
||||
WANT="$(curl -fsSL --proto '=https' "${BASE}/${VERSION}/manifest.json" \
|
||||
| tr -d '[:space:]' | grep -oE '"linux-x64"[^}]*' | grep -oE '[a-f0-9]{64}' | head -1)"
|
||||
[ "$(openssl dgst -sha256 ../claude | awk '{print $NF}')" = "${WANT}" ] \
|
||||
&& echo "sha256 OK" || { echo "checksum mismatch" >&2; rm -f ../claude; }
|
||||
gcloud auth configure-docker us-east5-docker.pkg.dev --quiet
|
||||
docker build --platform=linux/amd64 --provenance=false \
|
||||
-f ../Dockerfile -t "us-east5-docker.pkg.dev/<project>/claude-gateway/gateway:${VERSION}" ..
|
||||
docker push "us-east5-docker.pkg.dev/<project>/claude-gateway/gateway:${VERSION}"
|
||||
|
||||
# 3. Full apply:
|
||||
terraform apply
|
||||
```
|
||||
|
||||
(`../setup.sh` §3 automates the same download-and-verify.)
|
||||
|
||||
Set in `terraform.tfvars`:
|
||||
|
||||
- `project_id`, `region`
|
||||
- `image_tag` (after building/pushing — step 2 above)
|
||||
- **`oidc_client_secret`** — required (the Cloud Run service mounts `latest` of
|
||||
this secret; with no version the deploy fails). Terraform creates the
|
||||
secret + version from it.
|
||||
- `invoker_iam_disabled` / `allow_unauthenticated` — the gateway runs its own
|
||||
OIDC, so the Cloud Run invoker IAM check must be opened or disabled.
|
||||
**Preferred:** `invoker_iam_disabled = true` (no `allUsers` binding; works
|
||||
under Domain Restricted Sharing). **Fallback:** `allow_unauthenticated = true`
|
||||
grants `allUsers` `run.invoker` — fine on a normal org, but DRS orgs reject
|
||||
`allUsers` (set it `false` there, since an LB does **not** bypass the IAM
|
||||
check). If both paths are blocked by org policy, use the GKE track.
|
||||
- `ingress` — defaults to **internal-only** (no public URL). Claude Code's `/login`
|
||||
only accepts gateway hosts on private addresses, so public ingress cannot serve
|
||||
clients; the two-pass OAuth bootstrap must be completed from inside the VPC (or a
|
||||
PSC-connected corp network). See "Private access" below.
|
||||
|
||||
Tear down a trial with `terraform destroy`: set `deletion_protection = false`,
|
||||
run `terraform apply` to record that in state (the provider checks the value in
|
||||
**state**, not config, so destroy would still refuse otherwise), then `terraform
|
||||
destroy`. The destroy will stop at the VPC network
|
||||
because the Private Services Access peering is intentionally left in place
|
||||
(`deletion_policy = ABANDON` — see Guard rails below); finish by deleting the
|
||||
peering manually once the Cloud SQL instance is gone, then re-run destroy:
|
||||
|
||||
```bash
|
||||
gcloud services vpc-peerings delete --service=servicenetworking.googleapis.com \
|
||||
--network=cc-gateway-vpc --project=<project>
|
||||
terraform destroy
|
||||
```
|
||||
|
||||
## Guard rails
|
||||
|
||||
Tuned so accidental deletion is hard but greenfield teardown stays easy:
|
||||
|
||||
- `deletion_protection = true` (variable, default true) on Cloud SQL and Cloud Run —
|
||||
blocks accidental deletion; set `false` when you intend to `terraform destroy`.
|
||||
- `disable_on_destroy = false` on APIs — tearing down config never disables APIs.
|
||||
- `deletion_policy = ABANDON` on the PSA peering — never tears down the
|
||||
service-networking peering automatically (it's shared by every private-IP
|
||||
service on the VPC). On the dedicated VPC this module creates, that means
|
||||
`terraform destroy` stops at the network step; delete the peering manually
|
||||
per the teardown note above.
|
||||
- IAM uses non-authoritative `_member` resources, so other project/secret bindings
|
||||
are never clobbered.
|
||||
|
||||
## Private access (internal ingress) — the default
|
||||
|
||||
By default the service has **no public URL** (`ingress = "INGRESS_TRAFFIC_INTERNAL_ONLY"`),
|
||||
and there is no public-ingress option: Claude Code's `/login` rejects gateway hosts that
|
||||
resolve to public addresses, so public exposure cannot serve clients. Reach the service
|
||||
from inside the VPC, or via the private-access plumbing below.
|
||||
|
||||
With internal-only ingress, `public_url` stays the `run.app` URL (Google-managed cert) —
|
||||
**no load balancer or your own certificate required**. But internal ingress alone does
|
||||
**not** let corporate on-prem clients reach `run.app`; that needs **operator /
|
||||
network-team-owned** plumbing that Cloud Run does **not** create for you (validate it's in
|
||||
place before relying on internal ingress):
|
||||
|
||||
1. A **Private Service Connect endpoint** for Google APIs (an internal VIP in the VPC).
|
||||
2. A **Cloud DNS private zone for `run.app`** resolving `*.run.app` to that endpoint IP.
|
||||
3. **On-prem routing** to the endpoint over Cloud VPN / Interconnect.
|
||||
|
||||
This is normally managed centrally in the network/hub project, so the module does not
|
||||
provision it. See [Private networking and Cloud Run](https://cloud.google.com/run/docs/securing/private-networking).
|
||||
For a greenfield trial without this plumbing, complete the OAuth bootstrap from inside
|
||||
the VPC — e.g. a browser proxied through an in-VPC VM (SSH SOCKS tunnel over IAP).
|
||||
|
||||
For a **custom internal hostname or your own TLS cert**, use
|
||||
`INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER` and front the service with your own internal
|
||||
Application Load Balancer (also not provisioned by this module).
|
||||
|
||||
## Remote state (recommended for teams)
|
||||
|
||||
Add a backend so state is shared and locked (and out of git):
|
||||
|
||||
```hcl
|
||||
# backend.tf
|
||||
terraform {
|
||||
backend "gcs" {
|
||||
bucket = "<your-tf-state-bucket>"
|
||||
prefix = "claude-gateway/cloudrun"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## After deploy
|
||||
|
||||
- `terraform output service_url` / `oauth_redirect_uri`.
|
||||
- Register the redirect URI on the Google OAuth client and make sure
|
||||
`../gateway.yaml` `public_url` matches the host.
|
||||
- Notes: Terraform does not build the image. To ship a new gateway version,
|
||||
rerun the docker build/push under a new tag and bump `image_tag` — a bare
|
||||
re-apply under an unchanged tag does **not** roll a new revision (Cloud Run
|
||||
resolves the tag to a digest only at revision creation, and an unchanged
|
||||
`image` attribute means no new revision).
|
||||
@@ -0,0 +1,399 @@
|
||||
# Claude Gateway on Cloud Run — Terraform equivalent of setup.sh.
|
||||
# Section markers (§N) map to setup.sh and the walkthrough:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-on-gcp
|
||||
|
||||
locals {
|
||||
config_path = var.gateway_config_path != "" ? var.gateway_config_path : "${path.module}/../gateway.yaml"
|
||||
gateway_config = file(local.config_path)
|
||||
image = "${var.region}-docker.pkg.dev/${var.project_id}/${var.ar_repo}/${var.image_name}:${var.image_tag}"
|
||||
|
||||
apis = [
|
||||
"aiplatform.googleapis.com",
|
||||
"artifactregistry.googleapis.com",
|
||||
"cloudresourcemanager.googleapis.com",
|
||||
"sqladmin.googleapis.com",
|
||||
"secretmanager.googleapis.com",
|
||||
"iamcredentials.googleapis.com",
|
||||
"iam.googleapis.com",
|
||||
"compute.googleapis.com",
|
||||
"servicenetworking.googleapis.com",
|
||||
"run.googleapis.com",
|
||||
]
|
||||
}
|
||||
|
||||
# ── 1 Project & API setup ───────────────────────────────────────────────────
|
||||
resource "google_project_service" "apis" {
|
||||
for_each = toset(local.apis)
|
||||
project = var.project_id
|
||||
service = each.value
|
||||
# Don't disable APIs (or delete anything) when this config is torn down.
|
||||
disable_on_destroy = false
|
||||
disable_dependent_services = false
|
||||
}
|
||||
|
||||
# ── 2 Service account & IAM (least-privilege) ───────────────────────────────
|
||||
resource "google_service_account" "gateway" {
|
||||
project = var.project_id
|
||||
account_id = var.sa_name
|
||||
display_name = "Claude Gateway"
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# Non-authoritative (_member) so we never clobber other project bindings.
|
||||
#
|
||||
# Only aiplatform.user is granted: the gateway reaches Cloud SQL over the VPC at
|
||||
# its private IP with a password user (direct TCP via Direct VPC egress — see §7
|
||||
# below), not via the Cloud SQL Auth Proxy / connector, so it never calls
|
||||
# cloudsql.instances.connect and no roles/cloudsql.client grant is needed.
|
||||
# Direct private-IP keeps the gateway's store a plain postgres_url with no proxy
|
||||
# sidecar/socket plumbing, and the connection string is portable across Cloud
|
||||
# Run and GKE.
|
||||
resource "google_project_iam_member" "vertex" {
|
||||
project = var.project_id
|
||||
role = "roles/aiplatform.user" # Agent Platform inference
|
||||
member = "serviceAccount:${google_service_account.gateway.email}"
|
||||
}
|
||||
|
||||
# ── 3 Artifact Registry repo ────────────────────────────────────────────────
|
||||
# NOTE: image build/push is a separate step (see README) — Terraform only makes the repo.
|
||||
resource "google_artifact_registry_repository" "repo" {
|
||||
project = var.project_id
|
||||
location = var.region
|
||||
repository_id = var.ar_repo
|
||||
format = "DOCKER"
|
||||
description = "Claude Gateway container images"
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# ── 4 VPC + Private Services Access ──────────────────────────────────────────
|
||||
resource "google_compute_network" "vpc" {
|
||||
project = var.project_id
|
||||
name = var.vpc_network
|
||||
auto_create_subnetworks = false
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "subnet" {
|
||||
project = var.project_id
|
||||
name = var.subnet
|
||||
region = var.region
|
||||
network = google_compute_network.vpc.id
|
||||
ip_cidr_range = var.subnet_range
|
||||
}
|
||||
|
||||
resource "google_compute_global_address" "psa_range" {
|
||||
project = var.project_id
|
||||
name = "google-managed-services-${var.vpc_network}"
|
||||
purpose = "VPC_PEERING"
|
||||
address_type = "INTERNAL"
|
||||
prefix_length = var.psa_prefix_length
|
||||
network = google_compute_network.vpc.id
|
||||
}
|
||||
|
||||
resource "google_service_networking_connection" "psa" {
|
||||
network = google_compute_network.vpc.id
|
||||
service = "servicenetworking.googleapis.com"
|
||||
reserved_peering_ranges = [google_compute_global_address.psa_range.name]
|
||||
# ABANDON: on destroy, leave the producer peering in place (deleting it can hang
|
||||
# and would affect any other private-IP service on this VPC).
|
||||
deletion_policy = "ABANDON"
|
||||
# If the peering already exists (e.g. a previous apply failed partway), patch it
|
||||
# instead of failing the create.
|
||||
update_on_creation_fail = true
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# ── 4 Cloud SQL (private IP only) ───────────────────────────────────────────
|
||||
resource "google_sql_database_instance" "db" {
|
||||
project = var.project_id
|
||||
name = var.db_instance
|
||||
region = var.region
|
||||
database_version = var.db_version
|
||||
deletion_protection = var.deletion_protection
|
||||
depends_on = [google_service_networking_connection.psa]
|
||||
|
||||
settings {
|
||||
tier = var.db_tier
|
||||
ip_configuration {
|
||||
ipv4_enabled = false # private IP only (org policy: sql.restrictPublicIp)
|
||||
private_network = google_compute_network.vpc.id
|
||||
ssl_mode = "ENCRYPTED_ONLY"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_sql_database" "db" {
|
||||
project = var.project_id
|
||||
name = var.db_name
|
||||
instance = google_sql_database_instance.db.name
|
||||
}
|
||||
|
||||
# URL-safe (alphanumeric) so it drops cleanly into the connection string.
|
||||
# nosemgrep: terraform-generic-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "random_password" "db" {
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-gcp-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "google_sql_user" "gateway" {
|
||||
project = var.project_id
|
||||
name = var.db_user
|
||||
instance = google_sql_database_instance.db.name
|
||||
password = random_password.db.result
|
||||
# On destroy the role owns the tables it migrated at boot, so DROP ROLE can
|
||||
# fail (and races google_sql_database.db). ABANDON is harmless on the
|
||||
# greenfield teardown — the whole instance is deleted anyway.
|
||||
deletion_policy = "ABANDON"
|
||||
}
|
||||
|
||||
# ── 5/6 Secrets + secretAccessor ────────────────────────────────────────────
|
||||
# postgres-url: connection string built from the instance's private IP.
|
||||
resource "google_secret_manager_secret" "postgres_url" {
|
||||
project = var.project_id
|
||||
secret_id = var.secret_name
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-gcp-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "google_secret_manager_secret_version" "postgres_url" {
|
||||
secret = google_secret_manager_secret.postgres_url.id
|
||||
secret_data = "postgres://${var.db_user}:${random_password.db.result}@${google_sql_database_instance.db.private_ip_address}:5432/${var.db_name}?sslmode=require"
|
||||
}
|
||||
|
||||
# jwt: session signing key.
|
||||
# nosemgrep: terraform-generic-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "random_password" "jwt" {
|
||||
length = 48
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret" "jwt" {
|
||||
project = var.project_id
|
||||
secret_id = var.jwt_secret_name
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-gcp-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "google_secret_manager_secret_version" "jwt" {
|
||||
secret = google_secret_manager_secret.jwt.id
|
||||
secret_data = random_password.jwt.result
|
||||
}
|
||||
|
||||
# oidc client secret: operator-provided (from the Google OAuth client).
|
||||
resource "google_secret_manager_secret" "oidc" {
|
||||
project = var.project_id
|
||||
secret_id = var.oidc_secret_name
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-gcp-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "google_secret_manager_secret_version" "oidc" {
|
||||
count = var.oidc_client_secret != "" ? 1 : 0
|
||||
secret = google_secret_manager_secret.oidc.id
|
||||
secret_data = var.oidc_client_secret
|
||||
}
|
||||
|
||||
# Warn (not block) at plan time when the OIDC secret value isn't set: the Cloud
|
||||
# Run service mounts gateway-oidc-client-secret:latest unconditionally, so an
|
||||
# empty value with no out-of-band version means the apply fails late at
|
||||
# revision creation. A warning (not a precondition) keeps the documented
|
||||
# out-of-band-version mode usable.
|
||||
check "oidc_client_secret_set" {
|
||||
assert {
|
||||
condition = var.oidc_client_secret != ""
|
||||
error_message = "oidc_client_secret is empty — set it in terraform.tfvars, or add a version to the gateway-oidc-client-secret secret out-of-band before applying (the Cloud Run revision mounts it at :latest and will fail without one)."
|
||||
}
|
||||
}
|
||||
|
||||
# config: gateway.yaml. Guard mirrors the bash REPLACE_ME check (non-comment lines).
|
||||
resource "google_secret_manager_secret" "config" {
|
||||
project = var.project_id
|
||||
secret_id = var.config_secret_name
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-gcp-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "google_secret_manager_secret_version" "config" {
|
||||
secret = google_secret_manager_secret.config.id
|
||||
secret_data = local.gateway_config
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = length([
|
||||
for line in split("\n", local.gateway_config) :
|
||||
line
|
||||
if !startswith(trimspace(line), "#") && strcontains(line, "REPLACE_ME")
|
||||
]) == 0
|
||||
error_message = "gateway.yaml still has REPLACE_ME on a non-comment line — fill it in before applying."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "postgres_url" {
|
||||
project = var.project_id
|
||||
secret_id = google_secret_manager_secret.postgres_url.secret_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.gateway.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "jwt" {
|
||||
project = var.project_id
|
||||
secret_id = google_secret_manager_secret.jwt.secret_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.gateway.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "oidc" {
|
||||
project = var.project_id
|
||||
secret_id = google_secret_manager_secret.oidc.secret_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.gateway.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "config" {
|
||||
project = var.project_id
|
||||
secret_id = google_secret_manager_secret.config.secret_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.gateway.email}"
|
||||
}
|
||||
|
||||
# ── 7 Cloud Run (Direct VPC egress) ─────────────────────────────────────────
|
||||
resource "google_cloud_run_v2_service" "gateway" {
|
||||
project = var.project_id
|
||||
name = var.service_name
|
||||
location = var.region
|
||||
ingress = var.ingress
|
||||
invoker_iam_disabled = var.invoker_iam_disabled
|
||||
deletion_protection = var.deletion_protection
|
||||
|
||||
template {
|
||||
service_account = google_service_account.gateway.email
|
||||
scaling {
|
||||
min_instance_count = var.min_instances
|
||||
max_instance_count = var.max_instances
|
||||
}
|
||||
# Secrets are mounted at version=latest, so a config edit or secret
|
||||
# rotation alone wouldn't diff this resource and the warm min_instances=1
|
||||
# revision would keep the old values. Stamping a hash of the rendered
|
||||
# config + every managed secret value forces a new revision whenever any
|
||||
# of them change — without this, tainting random_password.db ALTERs the
|
||||
# SQL role to the new password while the running revision keeps the old
|
||||
# connection string and breaks on its next reconnect, and rotating the
|
||||
# OIDC client secret leaves login failing invalid_client.
|
||||
labels = {
|
||||
config-sha = substr(sha256(join("", [
|
||||
local.gateway_config,
|
||||
random_password.db.result,
|
||||
random_password.jwt.result,
|
||||
var.oidc_client_secret,
|
||||
])), 0, 63)
|
||||
}
|
||||
# Cloud Run's default 300s request timeout would cut off long streaming
|
||||
# /v1/messages responses mid-stream.
|
||||
timeout = "3600s"
|
||||
|
||||
vpc_access {
|
||||
network_interfaces {
|
||||
network = google_compute_network.vpc.id
|
||||
subnetwork = google_compute_subnetwork.subnet.id
|
||||
}
|
||||
egress = "PRIVATE_RANGES_ONLY" # public egress (Agent Platform, accounts.google.com) bypasses the VPC -> no Cloud NAT needed
|
||||
}
|
||||
|
||||
containers {
|
||||
image = local.image
|
||||
ports { container_port = 8080 }
|
||||
|
||||
# gateway.yaml mounted as a file at /etc/claude/gateway.yaml (alone in its dir).
|
||||
volume_mounts {
|
||||
name = "config"
|
||||
mount_path = "/etc/claude"
|
||||
}
|
||||
|
||||
# Cloud Run can't mount multiple secrets in one dir, so the rest are env vars
|
||||
# (gateway.yaml references them via ${ENV_VAR}).
|
||||
env {
|
||||
name = "GATEWAY_JWT_SECRET"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.jwt.secret_id
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "OIDC_CLIENT_SECRET"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.oidc.secret_id
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "GATEWAY_POSTGRES_URL"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.postgres_url.secret_id
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volumes {
|
||||
name = "config"
|
||||
secret {
|
||||
secret = google_secret_manager_secret.config.secret_id
|
||||
items {
|
||||
path = "gateway.yaml"
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
google_secret_manager_secret_iam_member.config,
|
||||
google_secret_manager_secret_iam_member.jwt,
|
||||
google_secret_manager_secret_iam_member.oidc,
|
||||
google_secret_manager_secret_iam_member.postgres_url,
|
||||
google_secret_manager_secret_version.config,
|
||||
google_secret_manager_secret_version.postgres_url,
|
||||
google_secret_manager_secret_version.jwt,
|
||||
google_secret_manager_secret_version.oidc,
|
||||
google_sql_database.db,
|
||||
google_sql_user.gateway,
|
||||
google_project_service.apis,
|
||||
]
|
||||
}
|
||||
|
||||
# Public access at the Cloud Run IAM layer — the gateway runs its own OIDC, so the
|
||||
# invoker check must be opened or disabled (real auth stays the gateway's SSO):
|
||||
# Preferred — disable it: invoker_iam_disabled=true on the service above. No allUsers
|
||||
# binding at all, and it works under Domain Restricted Sharing.
|
||||
# Fallback — open it: this allUsers run.invoker grant. Domain Restricted Sharing orgs
|
||||
# reject allUsers, and an LB does NOT bypass that (ingress is network-layer; the IAM
|
||||
# check still runs) — use invoker_iam_disabled, a DRS exception, or GKE.
|
||||
# Skipped when invoker_iam_disabled=true (the grant would be redundant, and DRS rejects it).
|
||||
resource "google_cloud_run_v2_service_iam_member" "public" {
|
||||
count = var.allow_unauthenticated && !var.invoker_iam_disabled ? 1 : 0
|
||||
project = var.project_id
|
||||
location = var.region
|
||||
name = google_cloud_run_v2_service.gateway.name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
output "service_url" {
|
||||
description = "Cloud Run service URL."
|
||||
value = google_cloud_run_v2_service.gateway.uri
|
||||
}
|
||||
|
||||
output "oauth_redirect_uri" {
|
||||
description = "Register this exact URI on the Google OAuth client, and ensure gateway.yaml public_url matches the host."
|
||||
value = "${google_cloud_run_v2_service.gateway.uri}/oauth/callback"
|
||||
}
|
||||
|
||||
output "service_account_email" {
|
||||
description = "Gateway runtime service account."
|
||||
value = google_service_account.gateway.email
|
||||
}
|
||||
|
||||
output "image" {
|
||||
description = "Image the service runs (build/push this separately — see README)."
|
||||
value = local.image
|
||||
}
|
||||
|
||||
output "db_connection_name" {
|
||||
description = "Cloud SQL instance connection name (project:region:instance)."
|
||||
value = google_sql_database_instance.db.connection_name
|
||||
}
|
||||
|
||||
output "db_private_ip" {
|
||||
description = "Cloud SQL private IP."
|
||||
value = google_sql_database_instance.db.private_ip_address
|
||||
}
|
||||
|
||||
output "public_invoker_granted" {
|
||||
description = "Whether the allUsers run.invoker binding was applied (false when invoker_iam_disabled handles public access instead, or on Domain-Restricted-Sharing orgs)."
|
||||
value = length(google_cloud_run_v2_service_iam_member.public) > 0
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copy to terraform.tfvars and edit. terraform.tfvars is gitignored (see .gitignore).
|
||||
|
||||
project_id = "your-gcp-project-id"
|
||||
region = "us-east5"
|
||||
|
||||
image_tag = "<version>" # REQUIRED — the Claude Code release version you build and push as linux/amd64 (see README Deploy)
|
||||
|
||||
# Public access at the Cloud Run IAM layer (the gateway runs its own OIDC):
|
||||
# Preferred — disable the invoker check: no allUsers binding, works under Domain
|
||||
# Restricted Sharing. Needs google provider >= 6.8 and the feature enabled for your org:
|
||||
# invoker_iam_disabled = true
|
||||
# Fallback — grant allUsers (fine on a normal org; Domain Restricted Sharing rejects it,
|
||||
# so there prefer invoker_iam_disabled, or use a DRS exception / GKE):
|
||||
allow_unauthenticated = true
|
||||
|
||||
# Network reachability — a separate axis from the IAM choice above. Default is internal-only:
|
||||
# no public URL (Claude Code's /login only accepts gateway hosts on private addresses, so
|
||||
# public ingress cannot serve clients); corp on-prem reaches run.app via a PSC endpoint +
|
||||
# private run.app DNS — see README "Private access" for the prerequisites. The only
|
||||
# alternative to the internal-only default:
|
||||
# ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" # only if you front it with your OWN internal ALB (custom hostname/cert; not provisioned here)
|
||||
|
||||
# Google OAuth client secret: REQUIRED — uncomment and set it (Terraform creates the
|
||||
# secret version; the Cloud Run service mounts `latest`, so without a version the
|
||||
# deploy fails). Leave empty only if you add the secret version out-of-band.
|
||||
# oidc_client_secret = "GOCSPX-..."
|
||||
@@ -0,0 +1,184 @@
|
||||
# Inputs — mirror the env-overridable knobs in setup.sh (same defaults).
|
||||
|
||||
variable "project_id" {
|
||||
description = "GCP project ID."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "Infra region for Artifact Registry, Cloud SQL, subnet, and Cloud Run. (Agent Platform region is set separately inside gateway.yaml.)"
|
||||
type = string
|
||||
default = "us-east5"
|
||||
}
|
||||
|
||||
# ── Service account (§2) ────────────────────────────────────────────────────
|
||||
variable "sa_name" {
|
||||
description = "Service account account_id (the part before @)."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
# ── Image (§3) ──────────────────────────────────────────────────────────────
|
||||
# Terraform creates the Artifact Registry repo but does NOT build/push the image
|
||||
# (that's a docker build step — see README). It references the image by tag.
|
||||
variable "ar_repo" {
|
||||
description = "Artifact Registry Docker repository ID."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
variable "image_name" {
|
||||
description = "Image name within the repo."
|
||||
type = string
|
||||
default = "gateway"
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Image tag — the Claude Code release version you build and push (must already be pushed as linux/amd64). See the README Deploy section for the build command."
|
||||
type = string
|
||||
validation {
|
||||
condition = can(regex("^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$", var.image_tag))
|
||||
error_message = "image_tag must be a valid OCI tag — set it to the Claude Code release version you pushed (the '<version>' in terraform.tfvars.example is a placeholder)."
|
||||
}
|
||||
}
|
||||
|
||||
# ── Networking (§4) ─────────────────────────────────────────────────────────
|
||||
variable "vpc_network" {
|
||||
description = "Custom VPC network name."
|
||||
type = string
|
||||
default = "cc-gateway-vpc"
|
||||
}
|
||||
|
||||
variable "subnet" {
|
||||
description = "Subnet name (Cloud Run Direct VPC egress attaches here)."
|
||||
type = string
|
||||
default = "cc-gateway-subnet"
|
||||
}
|
||||
|
||||
variable "subnet_range" {
|
||||
description = "Subnet primary CIDR."
|
||||
type = string
|
||||
default = "10.0.0.0/24"
|
||||
}
|
||||
|
||||
variable "psa_prefix_length" {
|
||||
description = "Prefix length for the Private Services Access allocated range (/16 is GCP's recommendation)."
|
||||
type = number
|
||||
default = 16
|
||||
}
|
||||
|
||||
# ── Cloud SQL (§4) ──────────────────────────────────────────────────────────
|
||||
variable "db_instance" {
|
||||
description = "Cloud SQL instance name."
|
||||
type = string
|
||||
default = "claude-gateway-db"
|
||||
}
|
||||
|
||||
variable "db_version" {
|
||||
description = "Postgres major version. The gateway supports PostgreSQL 14 or newer; 16 is the recommended default."
|
||||
type = string
|
||||
default = "POSTGRES_16"
|
||||
}
|
||||
|
||||
variable "db_tier" {
|
||||
description = "Cloud SQL machine tier."
|
||||
type = string
|
||||
default = "db-g1-small"
|
||||
}
|
||||
|
||||
variable "db_name" {
|
||||
description = "Database name."
|
||||
type = string
|
||||
default = "claude_gateway"
|
||||
}
|
||||
|
||||
variable "db_user" {
|
||||
description = "Database user (the gateway connects as this role)."
|
||||
type = string
|
||||
default = "gateway"
|
||||
}
|
||||
|
||||
# ── Secrets (§5 / §6) ─────────────────────────────────────────────────────
|
||||
variable "secret_name" {
|
||||
description = "Secret Manager secret holding the Postgres connection string."
|
||||
type = string
|
||||
default = "gateway-postgres-url"
|
||||
}
|
||||
|
||||
variable "jwt_secret_name" {
|
||||
description = "Secret Manager secret holding the session JWT signing key."
|
||||
type = string
|
||||
default = "gateway-jwt-secret"
|
||||
}
|
||||
|
||||
variable "oidc_secret_name" {
|
||||
description = "Secret Manager secret holding the Google OAuth client secret."
|
||||
type = string
|
||||
default = "gateway-oidc-client-secret"
|
||||
}
|
||||
|
||||
variable "config_secret_name" {
|
||||
description = "Secret Manager secret holding gateway.yaml (mounted at /etc/claude/gateway.yaml)."
|
||||
type = string
|
||||
default = "gateway-config"
|
||||
}
|
||||
|
||||
variable "oidc_client_secret" {
|
||||
description = "Google OAuth client secret value. Leave empty to NOT manage the version via Terraform (only if you add the secret version out-of-band — without one the deploy fails)."
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "gateway_config_path" {
|
||||
description = "Path to gateway.yaml. Empty = ../gateway.yaml relative to this module."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ── Cloud Run (§7) ──────────────────────────────────────────────────────────
|
||||
variable "service_name" {
|
||||
description = "Cloud Run service name."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
variable "min_instances" {
|
||||
description = "Minimum Cloud Run instances (1 avoids cold OIDC discovery)."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "max_instances" {
|
||||
description = "Maximum Cloud Run instances. Each instance opens a Postgres pool of up to 5 connections (the gateway's store.max_connections default) and db-g1-small caps at ~50 max_connections — keep max_instances × 5 below the DB tier's limit, or raise the tier before raising this."
|
||||
type = number
|
||||
default = 8
|
||||
}
|
||||
|
||||
variable "ingress" {
|
||||
description = "Cloud Run ingress — Claude Code's /login only accepts gateway hosts on private addresses, so public ingress cannot serve clients: INGRESS_TRAFFIC_INTERNAL_ONLY (default; no public URL — VPC-only; reaches corp on-prem only with the private-access prerequisites in the README; public_url stays the run.app URL, so no LB or custom cert needed) or INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER (front with your own internal ALB for a custom hostname/cert)."
|
||||
type = string
|
||||
default = "INGRESS_TRAFFIC_INTERNAL_ONLY"
|
||||
validation {
|
||||
condition = contains(["INGRESS_TRAFFIC_INTERNAL_ONLY", "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"], var.ingress)
|
||||
error_message = "ingress must be INGRESS_TRAFFIC_INTERNAL_ONLY or INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER — Claude Code only connects to gateways on private addresses."
|
||||
}
|
||||
}
|
||||
|
||||
variable "invoker_iam_disabled" {
|
||||
description = "PREFERRED public-access path: disable the Cloud Run invoker IAM check so requests reach the container with no allUsers binding (works under Domain Restricted Sharing). Real auth stays the gateway's own OIDC. When true, the allUsers grant below is skipped. May be blocked by org policy constraints/run.managed.requireInvokerIam, or unavailable for the org (\"invoker_iam_disabled is not currently available for your organization\") — then fall back to allow_unauthenticated. Requires google provider >= 6.8."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "allow_unauthenticated" {
|
||||
description = "Fallback public-access path: grant allUsers run.invoker (the gateway needs the IAM layer open for its own OIDC). Prefer invoker_iam_disabled. Domain Restricted Sharing orgs reject allUsers — set false there and use invoker_iam_disabled, a DRS exception, or GKE."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "deletion_protection" {
|
||||
description = "Provider-level deletion protection on Cloud SQL and Cloud Run. Keep true to avoid accidental deletion of the running deployment."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Provider + version pins for the Claude Gateway Cloud Run deployment.
|
||||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = ">= 6.8, < 7.0" # 6.8 adds invoker_iam_disabled on google_cloud_run_v2_service
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = ">= 3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# MDM Deployment Examples
|
||||
|
||||
Example templates for deploying Claude Code [managed settings](https://code.claude.com/docs/en/settings#settings-files) through Jamf, Iru (Kandji), Intune, or Group Policy. Use these as starting points — adjust them to fit your needs.
|
||||
|
||||
All templates encode the same minimal example (`permissions.disableBypassPermissionsMode`). See the [settings reference](https://code.claude.com/docs/en/settings#available-settings) for the full list of keys, and [`../settings`](../settings) for more complete example configurations.
|
||||
|
||||
|
||||
## Templates
|
||||
|
||||
> [!WARNING]
|
||||
> These examples are community-maintained templates which may be unsupported or incorrect. You are responsible for the correctness of your own deployment configuration.
|
||||
|
||||
| File | Use with |
|
||||
| :--- | :--- |
|
||||
| [`managed-settings.json`](./managed-settings.json) | Any platform. Deploy to the [system config directory](https://code.claude.com/docs/en/settings#settings-files). |
|
||||
| [`macos/com.anthropic.claudecode.plist`](./macos/com.anthropic.claudecode.plist) | Jamf or Iru (Kandji) **Custom Settings** payload. Preference domain: `com.anthropic.claudecode`. |
|
||||
| [`macos/com.anthropic.claudecode.mobileconfig`](./macos/com.anthropic.claudecode.mobileconfig) | Full configuration profile for local testing or MDMs that take a complete profile. |
|
||||
| [`windows/Set-ClaudeCodePolicy.ps1`](./windows/Set-ClaudeCodePolicy.ps1) | Intune **Platform scripts**. Writes `managed-settings.json` to `C:\Program Files\ClaudeCode\`. |
|
||||
| [`windows/ClaudeCode.admx`](./windows/ClaudeCode.admx) + [`en-US/ClaudeCode.adml`](./windows/en-US/ClaudeCode.adml) | Group Policy or Intune **Import ADMX**. Writes `HKLM\SOFTWARE\Policies\ClaudeCode\Settings` (REG_SZ, single-line JSON). |
|
||||
|
||||
## Tips
|
||||
- Replace the placeholder `PayloadUUID` and `PayloadOrganization` values in the `.mobileconfig` with your own (`uuidgen`)
|
||||
- Before deploying to your fleet, test on a single machine and confirm `/status` lists the source under **Setting sources** — e.g. `Enterprise managed settings (plist)` on macOS or `Enterprise managed settings (HKLM)` on Windows
|
||||
- Settings deployed this way sit at the top of the precedence order and cannot be overridden by users
|
||||
|
||||
## Full Documentation
|
||||
|
||||
See https://code.claude.com/docs/en/settings#settings-files for complete documentation on managed settings and settings precedence.
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Claude Code Managed Settings</string>
|
||||
<key>PayloadDescription</key>
|
||||
<string>Configures managed settings for Claude Code.</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.anthropic.claudecode.profile</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>Example Organization</string>
|
||||
<key>PayloadScope</key>
|
||||
<string>System</string>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>DC3CBC17-3330-4CDE-94AC-D2342E9C88A3</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Claude Code</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.anthropic.claudecode.profile.BEFD5F54-71FC-4012-82B2-94399A1E220B</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.ManagedClient.preferences</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>BEFD5F54-71FC-4012-82B2-94399A1E220B</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadContent</key>
|
||||
<dict>
|
||||
<key>com.anthropic.claudecode</key>
|
||||
<dict>
|
||||
<key>Forced</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>mcx_preference_settings</key>
|
||||
<dict>
|
||||
<key>permissions</key>
|
||||
<dict>
|
||||
<key>disableBypassPermissionsMode</key>
|
||||
<string>disable</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>permissions</key>
|
||||
<dict>
|
||||
<key>disableBypassPermissionsMode</key>
|
||||
<string>disable</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"permissions": {
|
||||
"disableBypassPermissionsMode": "disable"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions"
|
||||
revision="1.0" schemaVersion="1.0">
|
||||
<policyNamespaces>
|
||||
<target prefix="claudecode" namespace="Anthropic.Policies.ClaudeCode" />
|
||||
<using prefix="windows" namespace="Microsoft.Policies.Windows" />
|
||||
</policyNamespaces>
|
||||
<resources minRequiredRevision="1.0" />
|
||||
<categories>
|
||||
<category name="Cat_ClaudeCode" displayName="$(string.Cat_ClaudeCode)" />
|
||||
</categories>
|
||||
<policies>
|
||||
<policy name="ManagedSettings"
|
||||
class="Machine"
|
||||
displayName="$(string.ManagedSettings)"
|
||||
explainText="$(string.ManagedSettings_Explain)"
|
||||
presentation="$(presentation.ManagedSettings)"
|
||||
key="SOFTWARE\Policies\ClaudeCode">
|
||||
<parentCategory ref="Cat_ClaudeCode" />
|
||||
<supportedOn ref="windows:SUPPORTED_Windows_10_0" />
|
||||
<elements>
|
||||
<text id="SettingsJson" valueName="Settings" maxLength="1000000" required="true" />
|
||||
</elements>
|
||||
</policy>
|
||||
</policies>
|
||||
</policyDefinitions>
|
||||
@@ -0,0 +1,28 @@
|
||||
<#
|
||||
Deploys Claude Code managed settings as a JSON file.
|
||||
|
||||
Intune: Devices > Scripts and remediations > Platform scripts > Add (Windows 10 and later).
|
||||
Run this script using the logged on credentials: No
|
||||
Run script in 64 bit PowerShell Host: Yes
|
||||
|
||||
Claude Code reads C:\Program Files\ClaudeCode\managed-settings.json at startup
|
||||
and treats it as a managed policy source. Edit the JSON below to change the
|
||||
deployed settings; see https://code.claude.com/docs/en/settings for available keys.
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$dir = Join-Path $env:ProgramFiles 'ClaudeCode'
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
|
||||
$json = @'
|
||||
{
|
||||
"permissions": {
|
||||
"disableBypassPermissionsMode": "disable"
|
||||
}
|
||||
}
|
||||
'@
|
||||
|
||||
$path = Join-Path $dir 'managed-settings.json'
|
||||
[System.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding($false)))
|
||||
Write-Output "Wrote $path"
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions"
|
||||
revision="1.0" schemaVersion="1.0">
|
||||
<displayName>Claude Code</displayName>
|
||||
<description>Claude Code policy settings</description>
|
||||
<resources>
|
||||
<stringTable>
|
||||
<string id="Cat_ClaudeCode">Claude Code</string>
|
||||
<string id="ManagedSettings">Managed settings (JSON)</string>
|
||||
<string id="ManagedSettings_Explain">Configures managed settings for Claude Code.
|
||||
|
||||
Enter the full settings configuration as a single line of JSON. The value is stored as a REG_SZ string at HKLM\SOFTWARE\Policies\ClaudeCode\Settings and is applied at the highest precedence; users cannot override these settings.
|
||||
|
||||
Example:
|
||||
{"permissions":{"disableBypassPermissionsMode":"disable"}}
|
||||
|
||||
For the list of available settings keys, see https://code.claude.com/docs/en/settings.
|
||||
|
||||
If your configuration is large or you prefer to manage a JSON file directly, deploy C:\Program Files\ClaudeCode\managed-settings.json instead (see Set-ClaudeCodePolicy.ps1).</string>
|
||||
</stringTable>
|
||||
<presentationTable>
|
||||
<presentation id="ManagedSettings">
|
||||
<textBox refId="SettingsJson">
|
||||
<label>Settings JSON:</label>
|
||||
</textBox>
|
||||
</presentation>
|
||||
</presentationTable>
|
||||
</resources>
|
||||
</policyDefinitionResources>
|
||||
@@ -0,0 +1,35 @@
|
||||
# Settings Examples
|
||||
|
||||
Example Claude Code settings files, primarily intended for organization-wide deployments. Use these as starting points — adjust them to fit your needs.
|
||||
|
||||
These may be applied at any level of the [settings hierarchy](https://code.claude.com/docs/en/settings#settings-files), though certain properties only take effect if specified in enterprise settings (e.g. `strictKnownMarketplaces`, `allowManagedHooksOnly`, `allowManagedPermissionRulesOnly`).
|
||||
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
> [!WARNING]
|
||||
> These examples are community-maintained snippets which may be unsupported or incorrect. You are responsible for the correctness of your own settings configuration.
|
||||
|
||||
| Setting | [`settings-lax.json`](./settings-lax.json) | [`settings-strict.json`](./settings-strict.json) | [`settings-bash-sandbox.json`](./settings-bash-sandbox.json) |
|
||||
|---------|:---:|:---:|:---:|
|
||||
| Disable `--dangerously-skip-permissions` | ✅ | ✅ | |
|
||||
| Block plugin marketplaces | ✅ | ✅ | |
|
||||
| Block user and project-defined permission `allow` / `ask` / `deny` | | ✅ | ✅ |
|
||||
| Block user and project-defined hooks | | ✅ | |
|
||||
| Deny web fetch and search tools | | ✅ | |
|
||||
| Bash tool requires approval | | ✅ | |
|
||||
| Bash tool must run inside of sandbox | | | ✅ |
|
||||
|
||||
## Tips
|
||||
- Consider merging snippets of the above examples to reach your desired configuration
|
||||
- Settings files must be valid JSON
|
||||
- Before deploying configuration files to your organization, test them locally by applying to `managed-settings.json`, `settings.json` or `settings.local.json`
|
||||
- The `sandbox` property only applies to the `Bash` tool; it does not apply to other tools (like Read, Write, WebSearch, WebFetch, MCPs), hooks, or internal commands
|
||||
|
||||
## Deploying via MDM
|
||||
|
||||
To distribute these settings as enterprise-managed policy through Jamf, Iru (Kandji), Intune, or Group Policy, see the deployment templates in [`../mdm`](../mdm).
|
||||
|
||||
## Full Documentation
|
||||
|
||||
See https://code.claude.com/docs/en/settings for complete documentation on all available managed settings.
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"allowManagedPermissionRulesOnly": true,
|
||||
"sandbox": {
|
||||
"enabled": true,
|
||||
"autoAllowBashIfSandboxed": false,
|
||||
"allowUnsandboxedCommands": false,
|
||||
"excludedCommands": [],
|
||||
"network": {
|
||||
"allowUnixSockets": [],
|
||||
"allowAllUnixSockets": false,
|
||||
"allowLocalBinding": false,
|
||||
"allowedDomains": [],
|
||||
"httpProxyPort": null,
|
||||
"socksProxyPort": null
|
||||
},
|
||||
"enableWeakerNestedSandbox": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"permissions": {
|
||||
"disableBypassPermissionsMode": "disable"
|
||||
},
|
||||
"strictKnownMarketplaces": []
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"permissions": {
|
||||
"disableBypassPermissionsMode": "disable",
|
||||
"ask": [
|
||||
"Bash"
|
||||
],
|
||||
"deny": [
|
||||
"WebSearch",
|
||||
"WebFetch"
|
||||
]
|
||||
},
|
||||
"allowManagedPermissionRulesOnly": true,
|
||||
"allowManagedHooksOnly": true,
|
||||
"strictKnownMarketplaces": [],
|
||||
"sandbox": {
|
||||
"autoAllowBashIfSandboxed": false,
|
||||
"excludedCommands": [],
|
||||
"network": {
|
||||
"allowUnixSockets": [],
|
||||
"allowAllUnixSockets": false,
|
||||
"allowLocalBinding": false,
|
||||
"allowedDomains": [],
|
||||
"httpProxyPort": null,
|
||||
"socksProxyPort": null
|
||||
},
|
||||
"enableWeakerNestedSandbox": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<id>https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md</id>
|
||||
<title>Claude Code Changelog</title>
|
||||
<subtitle>Release notes for Claude Code</subtitle>
|
||||
<author><name>Anthropic</name></author>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md"/>
|
||||
<link rel="self" type="application/atom+xml" href="https://raw.githubusercontent.com/anthropics/claude-code/main/feed.xml"/>
|
||||
<updated>2026-07-24T17:14:14Z</updated>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.219</id>
|
||||
<title>Claude Code v2.1.219</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.219"/>
|
||||
<updated>2026-07-24T17:14:14Z</updated>
|
||||
<content type="html"><p>• Added Claude Opus 5 (claude-opus-5), now the default Opus model — 1M context, fast mode at $10/$50 per Mtok</p>
|
||||
<p>• Added sandbox.network.strictAllowlist setting to deny non-allowlisted hosts for sandboxed commands without prompting</p>
|
||||
<p>• Added DirectoryAdded hook that fires after /add-dir or the SDK register_repo_root control request registers a new working directory mid-session</p>
|
||||
<p>• Added mcp_server_errors to the headless stream-json init event, listing --mcp-config entries skipped by config validation; terminal runs print a startup warning</p>
|
||||
<p>• Added the workflowSizeGuideline settings key so the advisory Dynamic workflow size guideline can be set from any settings file; the /config row is hidden while one does</p>
|
||||
<p>• Added nested subagent forwarding in stream-json: subagents spawned at depth-2+ now appear when --forward-subagent-text is set, keyed by their spawning Agent tool_use id</p>
|
||||
<p>• Fixed claude -p text output dropping the answer already produced when a turn dies on a mid-stream API error</p>
|
||||
<p>• Added HTTP status and error text to claude mcp list and /mcp when a server fails to connect, and a warning for MCP config values with hidden leading or trailing whitespace</p>
|
||||
<p>• Fixed a permission you approved while a self-hosted runner was restarting being dropped when the session resumed, so the approved action now runs</p>
|
||||
<p>• Fixed the Fable model row showing "Requires usage credits" for plans that include it, when a stale cache had baked the label in</p>
|
||||
<p>• Fixed a SIGTERM arriving while a self-hosted runner was starting up leaving a stale active row until the lease expired; it now deregisters cleanly</p>
|
||||
<p>• Added structured failure categories to self-hosted runner spawn and session failures, so hook errors, runner crashes and config errors can be told apart</p>
|
||||
<p>• Fixed the /model picker showing the merged Opus row as plain "Opus" instead of "Opus (1M context)"</p>
|
||||
<p>• Fixed copy-on-select inside GNU screen printing base64 into the terminal instead of copying the selection</p>
|
||||
<p>• Fixed Remote Control clients keeping a stale fast-mode status after a model switch, reconnect, or failed org check</p>
|
||||
<p>• Fixed CLAUDE_CODE_GIT_BASH_PATH on Windows exiting or being used as bash when the path isn't a bash/sh binary; it's now ignored with a warning</p>
|
||||
<p>• Fixed Vim mode: pressing ← on an empty prompt now returns to the agent view from NORMAL mode, not just INSERT</p>
|
||||
<p>• Fixed screen-reader mode rewriting the entire input line on every keystroke instead of echoing only the typed character</p>
|
||||
<p>• Improved the "Remote Control is only available via api.anthropic.com" error to name the specific setting that caused it</p>
|
||||
<p>• Improved claude --teleport to show which repo your current checkout points at when it doesn't match the session's repo</p>
|
||||
<p>• Changed dynamic workflows to default to a medium size guideline (aim for fewer than 15 agents); pick another size or unrestricted with Dynamic workflow size in /config</p>
|
||||
<p>• Changed managed MCP allowlist/denylist ${VAR} entries to resolve from the startup environment and managed-settings env instead of settings-file env</p>
|
||||
<p>• Changed the /model picker to highlight only the newest model's name, so the highlight marks the new release rather than an arbitrary subset of the list</p>
|
||||
<p>• Added the current default workflow size to the running-workflow status line, with a pointer to /config for changing it</p>
|
||||
<p>• Removed Opus 4.7 from fast mode; /fast now applies to Opus 5 and Opus 4.8</p>
|
||||
<p>• Updated the claude-api skill to default to Claude Opus 5, with a migration path from Opus 4.8</p>
|
||||
<p>• Subagents can now spawn nested subagents up to depth 3 by default (was 1); set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=1 to disable nesting</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.218</id>
|
||||
<title>Claude Code v2.1.218</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.218"/>
|
||||
<updated>2026-07-22T21:24:49Z</updated>
|
||||
<content type="html"><p>• Changed /code-review to run as a background subagent, so review work no longer fills your conversation and keeps stacked slash commands as its review target</p>
|
||||
<p>• Added screen-reader announcements of deleted text for word and line deletions (Option+Delete, Ctrl+W, Cmd+Backspace, Ctrl+U, Ctrl+K) in --ax-screen-reader mode</p>
|
||||
<p>• Fixed Windows paths with \u-prefixed segments (like C:\Users\unicorn) being corrupted into CJK characters in tool inputs, which made those files inaccessible</p>
|
||||
<p>• Fixed the left arrow key discarding the conversation with no undo: presses right after editing now ask to confirm, and Esc in the agent view returns to the conversation it backgrounded</p>
|
||||
<p>• Fixed multi-line paste collapsing into one line with j in place of newlines in terminals that encode pasted newlines as Ctrl+J</p>
|
||||
<p>• Fixed /context reporting stale pre-compact token usage after compacting from the message picker</p>
|
||||
<p>• Fixed /ultrareview failing on descriptive arguments like "review my auth changes" — they now run a review of your current branch with the text applied as a note to the findings</p>
|
||||
<p>• Fixed /code-review ultra silently running a local review in non-interactive sessions — it now launches the cloud review</p>
|
||||
<p>• Fixed gateway spend metering to price Bedrock application-inference-profile ARNs and other config-mapped upstream model IDs at the configured model's rates</p>
|
||||
<p>• Fixed mojibake when a long IDE selection was truncated mid-emoji, and a case where a tool executor error could be silently dropped</p>
|
||||
<p>• Fixed an engine teardown race that could start and abandon a phantom turn, and made input pushed after close consistently rejected</p>
|
||||
<p>• Fixed spurious "[Request interrupted by user]" messages after interrupted tool calls, and an unpaired tool_use block left in the transcript when a tool aborted mid-response</p>
|
||||
<p>• Fixed VoiceOver reading "new line" instead of echoing the typed space at the end of the input in --ax-screen-reader mode</p>
|
||||
<p>• Fixed plugin and settings panels not moving the terminal cursor to the focused row, so screen readers and magnifiers can follow arrow-key navigation</p>
|
||||
<p>• Fixed crashes (maximum call stack exceeded) when a deeply nested watched directory tree was deleted or moved, and when rendering deeply nested UI trees</p>
|
||||
<p>• Fixed pull request events occasionally being lost when a session exited immediately after creating or linking a PR</p>
|
||||
<p>• Fixed the Bedrock setup wizard failing profile verification for assume-role profiles in partitioned AWS regions and on proxy-only networks</p>
|
||||
<p>• Fixed rare negative or incorrect turn duration measurements after a system clock adjustment by timing turns with a monotonic clock</p>
|
||||
<p>• Fixed the "N MCP servers need authentication" startup notice over-counting claude.ai connectors that aren't connected in claude.ai</p>
|
||||
<p>• Fixed prompt history entries being dropped or duplicated when history writes raced or failed</p>
|
||||
<p>• Fixed a retry loop that re-sent identical doomed requests after a context-overflow error with a large thinking budget; Ctrl+B backgrounding now applies the same background-shell caps as other paths</p>
|
||||
<p>• Fixed agent frontmatter hooks running from untrusted folders: hooks now require the agent file's own folder to have accepted workspace trust</p>
|
||||
<p>• Fixed fork-session lineage being lost after compaction in headless and SDK sessions</p>
|
||||
<p>• Fixed a resumed session failing every turn, or crashing on resume, when its history held a malformed delta attachment</p>
|
||||
<p>• Improved /ultrareview error feedback so Claude can correct an invalid argument instead of retrying it unchanged</p>
|
||||
<p>• Improved auto mode: the dangerous-rm, background-&amp;, and suspicious-Windows-path checks no longer open permission dialogs; the auto-mode classifier adjudicates them instead</p>
|
||||
<p>• Improved sandbox command restrictions for IDE interactions</p>
|
||||
<p>• Improved trust dialogs to name the repository root the grant covers</p>
|
||||
<p>• Changed /deep-research to start only when invoked manually; Claude no longer launches it on its own</p>
|
||||
<p>• Changed plan mode with auto to no longer prompt for Bash commands the static analyzer can't prove read-only; the auto-mode classifier judges them instead</p>
|
||||
<p>• Added an announcement when fast mode changes as a result of switching models via /config model=&lt;x&gt; or Remote Control</p>
|
||||
<p>• Changed server-managed settings so benign feature and cost toggles no longer trigger the settings-approval prompt</p>
|
||||
<p>• Changed agent markdown files to reject agent names containing :, which is reserved for plugin namespacing</p>
|
||||
<p>• Changed skills with context: fork to run in the background by default; opt out per skill with background: false</p>
|
||||
<p>• Added yes/no/on/off/1/0 (case-insensitive) as accepted values for skill and plugin frontmatter booleans, alongside true/false</p>
|
||||
<p>• Fixed remote sessions continuing to send heartbeats after their worker was replaced, which left long-lived desktop and IDE processes retrying a rejected request every few seconds forever</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.217</id>
|
||||
<title>Claude Code v2.1.217</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.217"/>
|
||||
<updated>2026-07-21T21:35:04Z</updated>
|
||||
<content type="html"><p>• Added emoji shortcode autocomplete in the prompt input: type :heart: to insert ❤️, or :hea for suggestions — disable with the emojiCompletionEnabled setting</p>
|
||||
<p>• Added warnings when transcript writes are failing (e.g. disk full) or when session saving is off due to an inherited environment variable, instead of losing transcripts silently</p>
|
||||
<p>• Fixed a memory leak where truncated MCP tool outputs kept the full untruncated result in memory for the rest of the session</p>
|
||||
<p>• Fixed Windows auto-update failures that could leave claude.exe missing; failed updates now restore the preserved executable automatically</p>
|
||||
<p>• Fixed background session isolation not canonicalizing symlinked working directories, which could let sessions escape their workspace folder</p>
|
||||
<p>• Fixed auto-compact never triggering for Claude Opus 4.8 on Bedrock and /compact failing once over the limit</p>
|
||||
<p>• Fixed corporate mTLS, TLS-verify, OAuth scope, and proxy settings being ignored in Claude Desktop sessions</p>
|
||||
<p>• Fixed screen reader mode's startup announcement being cut off by the first prompt render, and the thinking status row re-rendering every few seconds to update elapsed time and token counts</p>
|
||||
<p>• Fixed managed settings that set OTEL_EXPORTER_OTLP_ENDPOINT not governing all signals — lower-scope signal-specific overrides no longer redirect telemetry away from the managed endpoint</p>
|
||||
<p>• Fixed --resume/--continue and /resume failing with a TypeError when a transcript has a malformed attachment entry</p>
|
||||
<p>• Fixed Remote Control sessions not showing a pending permission prompt or dialog to viewers that connected after it appeared</p>
|
||||
<p>• Fixed background shells sometimes becoming impossible to stop after a session is sent to the background (/background or ←) or when the session exits on a heavily loaded machine, most visible on Windows</p>
|
||||
<p>• Fixed a CLAUDE.md or SKILL.md paths frontmatter value with many brace groups OOM-killing or stalling the CLI at startup — brace expansion is now budget-bounded</p>
|
||||
<p>• Fixed the transcript preview sitting flush against the input area when attaching to a starting background session; it now leaves the same one-line gap as the live layout, so the transcript no longer shifts when the session takes over</p>
|
||||
<p>• Improved footer PR badge links to be clickable hyperlinks even when terminal support can't be detected (e.g. over ssh/tmux); set FORCE_HYPERLINK=0 to opt out</p>
|
||||
<p>• Changed the login-expiry warning to appear 3 days before expiry instead of 5</p>
|
||||
<p>• Capped the frontend-design plugin suggestion tip at 3 lifetime impressions instead of repeating indefinitely</p>
|
||||
<p>• Added a cap on concurrently-running subagents (default 20, override with CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS) so one message can't fan out unbounded background agents</p>
|
||||
<p>• Changed subagents to no longer spawn nested subagents by default; set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH to allow deeper nesting</p>
|
||||
<p>• Fixed --max-budget-usd not stopping background subagents: once the cap is reached, new spawns are denied and running background agents are halted</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.216</id>
|
||||
<title>Claude Code v2.1.216</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.216"/>
|
||||
<updated>2026-07-20T22:13:53Z</updated>
|
||||
<content type="html"><p>• Added sandbox.filesystem.disabled setting to skip filesystem isolation while keeping network egress control</p>
|
||||
<p>• Fixed a slowdown in long sessions where message normalization cost grew quadratically with the number of turns, causing multi-second stalls and slow resumes</p>
|
||||
<p>• Fixed auto mode denying commands with "HTTP 401" classifier errors after the OAuth token expired or rotated mid-session</p>
|
||||
<p>• Fixed AskUserQuestion telling Claude to continue even when your answer asked it to wait or explain first — free-text answers now get neutral wording</p>
|
||||
<p>• Fixed Claude Code on the web re-asking the same question and dropping your answer after the session sat idle for a few minutes</p>
|
||||
<p>• Fixed @-mentions silently attaching nothing after file-modifying hooks, vim dot-repeat of c-operators and paste, statusline running twice on resume, and resume-picker hangs on failure</p>
|
||||
<p>• Fixed resumed background agent sessions reverting to the default agent: the agent's prompt and tool restrictions are now restored</p>
|
||||
<p>• Fixed worktree-isolated subagents redirecting git into the shared checkout via git -C, --git-dir, or GIT_DIR/GIT_WORK_TREE</p>
|
||||
<p>• Fixed worktree sessions landing in another project's leftover worktree when the working directory did not match the selected project</p>
|
||||
<p>• Fixed background sessions whose worktree has no git repository being undeletable</p>
|
||||
<p>• Fixed claude daemon stop --any potentially terminating an unrelated process via a stale legacy daemon lockfile</p>
|
||||
<p>• Fixed Esc-Esc at an idle prompt not opening the rewind picker in long-running sessions with background tasks</p>
|
||||
<p>• Fixed Bash command permission checking for compound statements with redirects inside &amp;&amp; lists or negations</p>
|
||||
<p>• Fixed pressing Ctrl+X twice in the agent list failing to delete a session, and deleted sessions reappearing when their background worker had died</p>
|
||||
<p>• Fixed background subagents getting cancelled when a high-priority message arrives during their startup window</p>
|
||||
<p>• Fixed mouse and focus garbage in the terminal while a GUI editor from /memory, /plan, /keybindings, or Ctrl+G is open; /memory no longer waits for the editor to close</p>
|
||||
<p>• Fixed Claude-in-Chrome 403-looping on reconnect when the session's OAuth token lacks a required scope</p>
|
||||
<p>• Fixed workflow saves and scheduled-task writes following a symlink at .claude, which could redirect writes outside the project</p>
|
||||
<p>• Fixed MCP re-authenticate revoking working credentials before the new sign-in succeeds, and the reconnect needs-auth message in background sessions pointing at an unusable command</p>
|
||||
<p>• Fixed read-only commands on Windows accessing network paths without a permission prompt</p>
|
||||
<p>• Fixed Bash command parsing of non-ASCII characters to match real shell word boundaries</p>
|
||||
<p>• Fixed PowerShell tool permission validation of commands containing invisible Unicode characters</p>
|
||||
<p>• Fixed dialogs in fullscreen mode stretching past the right-hand edge of their panel</p>
|
||||
<p>• Fixed the /config settings list in fullscreen mode clipping its keyboard-hint footer</p>
|
||||
<p>• Fixed the transcript-mode (Ctrl+O) footer hint wrapping on terminals narrower than 104 columns</p>
|
||||
<p>• Fixed the Prometheus metrics endpoint (OTEL_METRICS_EXPORTER=prometheus) emitting invalid # UNIT lines</p>
|
||||
<p>• Fixed skills and commands changed during a session not appearing in the slash menu until restart</p>
|
||||
<p>• Fixed plugin skills with a name frontmatter field losing their plugin prefix in slash-command autocomplete</p>
|
||||
<p>• Fixed telemetry misreporting permission denials: failed permission-prompt requests no longer count as user rejections, and user interrupts are now reported as user aborts instead of rejections</p>
|
||||
<p>• Improved the /fork confirmation to one line with the new session's name, claude attach id, and a note when the copy shares your checkout</p>
|
||||
<p>• Improved validation of git and gh command arguments in the PowerShell tool</p>
|
||||
<p>• Improved the /ultrareview diff-too-large error to show configured limits, measured diff size, and largest contributing files</p>
|
||||
<p>• Improved /code-review ultra empty-diff message to name the exact base ref and suggest passing an explicit base</p>
|
||||
<p>• Improved the spend limit adjustment prompt to show the server's reason when a spend limit change is rejected</p>
|
||||
<p>• /context now shows an explicit warning when the conversation exceeds the context window, and a failed /compact displays as an error</p>
|
||||
<p>• /rewind no longer restores or deletes files through symlinks or hard links at tracked paths and reports how many paths it skipped</p>
|
||||
<p>• Background sessions: /mcp and /install-github-app now park a "needs input" request in the agent view when no client is attached</p>
|
||||
<p>• Updated the bundled dataviz skill: reordered the default chart palette and fixed guidance that suggested direct labels for four-series charts</p>
|
||||
<p>• [VSCode] Fixed right-to-left text (Arabic, Hebrew, Persian) rendering in the wrong order when mixed with English or code</p>
|
||||
<p>• Fixed cloud sessions dropping the in-flight message when the session's container restarts mid-turn — the interrupted turn now re-runs on resume instead of leaving the session unresponsive</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.215</id>
|
||||
<title>Claude Code v2.1.215</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.215"/>
|
||||
<updated>2026-07-19T02:55:54Z</updated>
|
||||
<content type="html"><p>• Claude no longer runs the /verify and /code-review skills on its own; invoke them with /verify or /code-review when you want them</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.214</id>
|
||||
<title>Claude Code v2.1.214</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.214"/>
|
||||
<updated>2026-07-18T01:20:23Z</updated>
|
||||
<content type="html"><p>• Fixed single-segment dir/ allow rules like Edit(src/) auto-approving writes to nested dir/ directories anywhere in the tree instead of only &lt;cwd&gt;/dir</p>
|
||||
<p>• Fixed a permission-check bypass affecting commands run in Windows PowerShell 5.1 sessions</p>
|
||||
<p>• Fixed Bash permission checks to fail closed on file-descriptor redirect forms that bash parses differently than the permission analyzer</p>
|
||||
<p>• Fixed Bash permission checks misjudging very long commands — commands over 10,000 characters now always prompt instead of running automatically</p>
|
||||
<p>• Fixed Bash permission checks treating zsh variable subscripts and modifiers in [[ ]] comparisons as inert text — these commands now prompt for approval</p>
|
||||
<p>• Fixed Bash permission checks to no longer auto-approve certain help and man commands that could run unsafe options, command substitutions, or backslash paths</p>
|
||||
<p>• Fixed permission prompts on remote sessions that could proceed before the local confirmation dialog</p>
|
||||
<p>• Added the EndConversation tool: Claude can end sessions with highly abusive users or jailbreak attempts, as on claude.ai since 2025 — see https://www.anthropic.com/research/end-subset-conversations</p>
|
||||
<p>• Added a periodic progress heartbeat for long-running tool calls that previously went silent</p>
|
||||
<p>• Added an ISO modified timestamp to memory file frontmatter</p>
|
||||
<p>• Added message.uuid, client_request_id, and tool_source attributes to OpenTelemetry log events for message-level correlation and tool provenance</p>
|
||||
<p>• Added CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH to configure the 60 KB truncation limit on OpenTelemetry content attributes</p>
|
||||
<p>• Added reasoning effort to the subagentStatusLine payload, so custom agent rows can render model and effort</p>
|
||||
<p>• Added permission prompts for docker commands (including the Podman docker shim) carrying daemon-redirect flags (--url, --connection, --identity, and Podman's remote mode) that previously ran without one</p>
|
||||
<p>• Fixed a crash when a GrowthBook feature evaluates to null, and a bug where a malformed flag payload could wipe the cached feature flags</p>
|
||||
<p>• Fixed Bash tool killing the Claude session when a pkill -f pattern accidentally matched the CLI's own process (Linux)</p>
|
||||
<p>• Fixed unbounded memory growth when --settings points at a device file or multi-GB file; oversized (&gt;2 MiB) settings files now fail at startup with a clear error</p>
|
||||
<p>• Fixed streaming turns failing with "Socket is closed" behind corporate proxies on Windows</p>
|
||||
<p>• Fixed stream-json output truncation at exit for slow-reading SDK/pipeline consumers; the exit drain now scales with queued bytes instead of a flat 2s cap</p>
|
||||
<p>• Fixed scheduled tasks refusing their own configured prompt as untrusted input — the fired prompt is now delivered as the session's assigned task</p>
|
||||
<p>• Fixed PowerShell tool commands hanging until timeout when a child process waited on standard input (Windows)</p>
|
||||
<p>• Fixed Python scripts under the PowerShell tool crashing with UnicodeDecodeError when reading non-UTF-8 data from standard input (Windows)</p>
|
||||
<p>• Fixed Python scripts run via the PowerShell tool crashing with UnicodeEncodeError on non-ASCII output, and PowerShell 7 error messages containing raw ANSI escape sequences (Windows)</p>
|
||||
<p>• Fixed the PowerShell tool reporting where.exe, fc.exe, and diff.exe as errors when they return a valid negative answer (Windows)</p>
|
||||
<p>• Fixed &gt; and &gt;&gt; under the PowerShell tool on Windows PowerShell 5.1 writing UTF-16LE files that other tools couldn't read as UTF-8</p>
|
||||
<p>• Fixed a displaced background daemon deleting its successor's control socket on shutdown, which made the next client kill the healthy replacement daemon</p>
|
||||
<p>• Fixed background sessions parked with ← or /background and left idle keeping the background daemon and a worker process alive indefinitely</p>
|
||||
<p>• Fixed completed background sessions being impossible to remove via claude rm or the agent view once the background service had gone idle</p>
|
||||
<p>• Fixed background sessions dispatched from a non-git folder being impossible to delete from the agents view</p>
|
||||
<p>• Fixed reopening a stopped background session failing to restore its saved conversation when an unreadable folder exists in the session store</p>
|
||||
<p>• Fixed the Remote Control "session ready" push notification firing for sessions where Remote Control was not explicitly enabled</p>
|
||||
<p>• Fixed /install-github-app and the /mcp settings menu being blocked in agent-view sessions — they're now refused only in background sessions with no terminal attached</p>
|
||||
<p>• Fixed plugins enabled via the --settings CLI flag not loading (regression since v2.1.181)</p>
|
||||
<p>• Fixed feature flags going stale in long-running sessions after the OAuth token rotates</p>
|
||||
<p>• Fixed /ultrareview refusing to run in repos with no merge base — it now offers to review all tracked files</p>
|
||||
<p>• Fixed claude update and claude doctor hanging silently, and the /status System diagnostics section going blank, when a shell-config path is a directory</p>
|
||||
<p>• Fixed memory frontmatter values being silently truncated at an inline # when memory files are saved</p>
|
||||
<p>• Fixed session cost and token telemetry double-counting on streams that emit multiple cumulative message_delta frames</p>
|
||||
<p>• Fixed a spurious "check your network" warning that appeared while the advisor was thinking</p>
|
||||
<p>• Fixed hooks with exit code 2 not blocking as documented when the hook's stdout JSON fails schema validation</p>
|
||||
<p>• Fixed OTel log events emitted outside the turn's async context missing the interaction span's trace context</p>
|
||||
<p>• Fixed MCP transient errors during prompts/resources refresh clearing the server's slash commands and resources</p>
|
||||
<p>• Improved the claude rc workspace-trust error in the home directory to say trust there is never saved and to suggest running from a project directory</p>
|
||||
<p>• Changed single-segment dir/ hook if: conditions to match only &lt;cwd&gt;/dir; write /dir/** for any-depth matching. deny/ask permission rules keep their any-depth match.</p>
|
||||
<p>• Changed file commands using -m/--magic-file or -f/--files-from to require permission instead of being auto-allowed as read-only</p>
|
||||
<p>• Changed keep-alive connection pooling to disable after a stale-connection error, so retries open a fresh socket</p>
|
||||
<p>• Changed SessionStart hooks to report source "fork" when a session begins as a fork instead of "resume"</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.212</id>
|
||||
<title>Claude Code v2.1.212</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.212"/>
|
||||
<updated>2026-07-17T00:26:21Z</updated>
|
||||
<content type="html"><p>• /fork now copies your conversation into a new background session (its own row in claude agents) while you keep working; the in-session subagent it used to launch is now /subtask</p>
|
||||
<p>• Added claude auto-mode reset to restore the default auto-mode configuration, with a confirmation prompt (pass --yes to skip)</p>
|
||||
<p>• Added a session-wide limit on WebSearch tool calls (default 200, tunable via CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION) to stop runaway search loops</p>
|
||||
<p>• Added a per-session cap on subagent spawns (default 200, override with CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION) to stop runaway delegation loops; /clear resets the budget</p>
|
||||
<p>• MCP tool calls running longer than 2 minutes now move to the background automatically so the session stays usable; configure the threshold or disable with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS</p>
|
||||
<p>• Typing /resume in the agent view now opens a picker of past sessions — including sessions deleted from the list — and resumes your pick as a background session</p>
|
||||
<p>• Fixed plan mode auto-running file-modifying Bash commands (e.g. touch, rm) without a permission prompt or SDK canUseTool callback</p>
|
||||
<p>• Fixed worktree creation following a repository-committed symlink at .claude/worktrees, which could create files outside the repository</p>
|
||||
<p>• Fixed a continue:false hook's halt being dropped when the tool fails or completes mid-stream, and hook infrastructure errors being misreported as user rejections</p>
|
||||
<p>• Fixed SIGTERM during a running Bash tool orphaning the command's process tree in print/SDK mode; the CLI now aborts the turn, kills the tree, and exits 143</p>
|
||||
<p>• Fixed /background and claude --bg failing with "EUNKNOWN: unknown error, uv_spawn" on Windows when Group Policy blocks PowerShell 5.1; the daemon now prefers PowerShell 7</p>
|
||||
<p>• Fixed shell mode (!) not executing commands containing file paths while the path autocomplete popup was open</p>
|
||||
<p>• Fixed auto-mode denial notifications rendering broken characters when a long denial reason was truncated mid-emoji</p>
|
||||
<p>• Fixed Ctrl+J not inserting a newline in the agent view dispatch input on terminals with extended key reporting, and surfaced the newline shortcut in the ? help overlay</p>
|
||||
<p>• Fixed /ultrareview rejecting PR references like #123, PR 123, and pasted PR URLs; error hints now name the command you actually typed</p>
|
||||
<p>• Fixed /ultrareview &lt;branch&gt; not fetching the branch from origin when it exists remotely; it now suggests the closest branch name on typos</p>
|
||||
<p>• Fixed /ultrareview skipping the billing confirmation in a new conversation after /clear</p>
|
||||
<p>• Fixed /ultrareview's "not a git repository" error on Claude Desktop now suggesting the project's repository folder instead of terminal commands</p>
|
||||
<p>• Fixed hosted (host-managed) sessions failing at startup when repository settings configured mTLS certs, extra CA bundles, or OAuth scopes; these transport settings are now ignored with a warning</p>
|
||||
<p>• Fixed a spurious "File has not been read yet" error when editing a file that had been read with offset/limit before resuming a session</p>
|
||||
<p>• Fixed ExitWorktree failing with "no active EnterWorktree session" after resuming a session with --continue/--resume in print/SDK mode</p>
|
||||
<p>• Fixed the workflow agent grid staying empty for Remote Control clients that join a session mid-run</p>
|
||||
<p>• Fixed streaming-mode control requests being marked complete before their handler finished, which could lose the request on session restart</p>
|
||||
<p>• Fixed background sessions created with /fork losing their live-parent protection after a state write failure</p>
|
||||
<p>• Fixed reopening a stopped background session from the agent view failing silently — it now resumes the session, or shows why it can't and lets you force a restart</p>
|
||||
<p>• Fixed agent teams: a stopping teammate could send the leader duplicate idle notifications when team initialization re-ran within a session</p>
|
||||
<p>• Fixed the plan-approval dialog footer splitting "ctrl+g to edit in &lt;editor&gt;" apart when the file path is long</p>
|
||||
<p>• Fixed the welcome banner keeping its old panel widths after a combined width+height terminal resize in fullscreen mode</p>
|
||||
<p>• Fixed diff previews losing their line numbers and +/- markers in narrow layouts</p>
|
||||
<p>• Fixed @-mentions attaching nothing after a partial file read, plugin uninstall targeting the wrong marketplace, and false "Command timed out" on exit code 143</p>
|
||||
<p>• Fixed OpenTelemetry HTTP exports being rejected with 411/400 by Azure Monitor and other endpoints that don't accept chunked transfer encoding</p>
|
||||
<p>• Fixed OTLP event log records missing trace_id/span_id when TRACEPARENT is set in SDK/headless mode</p>
|
||||
<p>• Fixed conversations with many images incorrectly failing with "Request too large" errors, and improved the error message to explain the actual cause</p>
|
||||
<p>• Fixed web search and web fetch returning "API Error" text as search results or page content when the API was overloaded</p>
|
||||
<p>• Improved web search and web fetch reliability by retrying 529 errors and rate-limited requests with bounded backoff</p>
|
||||
<p>• Improved prompt caching: the mid-conversation system block now works behind LLM gateways and custom base URLs (Bedrock, Vertex, 1P)</p>
|
||||
<p>• Improved background agent attach: cold-attaching now instantly shows the formatted transcript while the session boots, instead of a blank wait</p>
|
||||
<p>• Reduced token usage in inter-agent messaging: SendMessage bodies are no longer duplicated into replayed history and tool results</p>
|
||||
<p>• Changed /fork to name the copy after your prompt when the session has no title, so the row is recognizable in the agent view</p>
|
||||
<p>• Changed bare /btw to reopen the side-question panel on your most recent exchange so you can browse earlier answers</p>
|
||||
<p>• Changed the ← footer hint to pulse N done for a moment when a background agent finishes while nothing needs your input</p>
|
||||
<p>• Deprecated the Task tool's mode parameter (now ignored); subagents inherit the parent session's permission mode by default</p>
|
||||
<p>• Changed Enterprise forceLoginMethod to be enforced for VS Code extension, SDK, setup-token, and install-github-app logins, not just the terminal</p>
|
||||
<p>• Changed session transcripts to record the reasoning effort level on each assistant message</p>
|
||||
<p>• Changed headless/SDK sessions to apply a set_model control request mid-turn; the next model round-trip uses the new model instead of waiting for the next turn</p>
|
||||
<p>• Changed agent view / claude agents --json: sessions waiting on a sandbox, MCP-input, or managed-settings prompt now show as "Needs input" instead of "Working"</p>
|
||||
<p>• Updated the auth status panel title from "Cloud authentication" to "Authentication"</p>
|
||||
<p>• Corrected an earlier release note (2.1.200): tmux through the 3.6 series lacks synchronized output; newer tmux with support is detected automatically</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.211</id>
|
||||
<title>Claude Code v2.1.211</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.211"/>
|
||||
<updated>2026-07-15T23:02:29Z</updated>
|
||||
<content type="html"><p>• Added --forward-subagent-text flag and CLAUDE_CODE_FORWARD_SUBAGENT_TEXT environment variable to include subagent text and thinking in stream-json output</p>
|
||||
<p>• Fixed permission previews relayed to chat channels not neutralizing bidirectional-override, zero-width, and look-alike quote characters, so tool inputs cannot visually alter the approval message</p>
|
||||
<p>• Fixed auto mode overriding a PreToolUse hook's ask decision for unsandboxed Bash — a hook ask now floors the decision at a prompt</p>
|
||||
<p>• Fixed parallel Claude Code sessions all logging out simultaneously after wake-from-sleep when many sessions share one credential store</p>
|
||||
<p>• Fixed plugin MCP servers not reconnecting after an idle web session woke, leaving MCP calls failing until the next message</p>
|
||||
<p>• Fixed Claude Code on Vertex and Bedrock attempting the default Opus model at startup and printing a spurious fallback notice when a model is explicitly configured</p>
|
||||
<p>• Fixed subagents spawned with an explicit model override reverting to the parent's model when resumed or sent a follow-up message</p>
|
||||
<p>• Fixed nested .claude/rules/*.md files loading even when setting sources exclude project settings</p>
|
||||
<p>• Fixed file upload validation: filenames ending in a DOS device suffix (.prn) or trailing dot are now accepted, and files with multiple hard links are refused</p>
|
||||
<p>• Fixed file uploads to Claude in Chrome from remote and CLI sessions</p>
|
||||
<p>• Fixed edits that leave the input as "?" being silently swallowed and toggling the shortcuts panel</p>
|
||||
<p>• Fixed a startup hang when the Claude in Chrome extension is enabled but Chrome is not running</p>
|
||||
<p>• Fixed a 300ms delay revealing async content (Settings tabs, Stats, diff views, and other loading states)</p>
|
||||
<p>• Fixed reopening a just-stopped background session from the agents view starting a blank conversation under the same session id</p>
|
||||
<p>• Fixed /loop hiding the session from /resume after a single use</p>
|
||||
<p>• Fixed screen reader users losing the audible terminal bell after /terminal-setup or onboarding terminal setup</p>
|
||||
<p>• Fixed background jobs on LLM gateway auth (ANTHROPIC_AUTH_TOKEN + ANTHROPIC_BASE_URL) coming back "Not logged in" after the daemon respawns them</p>
|
||||
<p>• Fixed claude agents jobs becoming permanently undeletable when git no longer recognizes their worktree — the row now shows why the delete was refused instead of silently reappearing</p>
|
||||
<p>• Fixed /clear not resetting the session cost counter — the statusline's cost now starts at $0 after /clear</p>
|
||||
<p>• Fixed Claude in Chrome setup pages failing to open in the browser on Windows</p>
|
||||
<p>• Fixed headless print-mode sessions on Windows crashing or silently exiting when stdin is unreadable</p>
|
||||
<p>• Fixed background session titles in the agents view showing the naming model's refusal text when the prompt contains a link</p>
|
||||
<p>• Fixed background agents killed by the user auto-respawning, and revived agents re-running stale prompts from old sessions</p>
|
||||
<p>• Fixed routines with no schedule reporting a next run time in the year 1</p>
|
||||
<p>• Hardened synced skill/plugin directory naming on Windows and kept CCR web fetch/search proxies working after /clear</p>
|
||||
<p>• Improved terminal layout and rendering performance</p>
|
||||
<p>• Improved background agent result reporting — Claude now reports the status of still-running agents and waits for the real completion instead of fabricating results</p>
|
||||
<p>• Improved the memory index over-limit warning to measure only loaded content, excluding frontmatter and HTML comments</p>
|
||||
<p>• Updated integer environment variables (timeouts, token budgets, retry counts) to accept scientific notation and digit-separator spellings like 1e6 and 64_000</p>
|
||||
<p>• Updated documentation links to the current docs sites</p>
|
||||
<p>• Changed "always allow" permission rules to save at the repository root, so approvals granted in a git worktree persist across sessions and worktrees</p>
|
||||
<p>• Changed /usage-credits to ask for confirmation before sending a request to organization admins</p>
|
||||
<p>• Changed Vim mode s and S (substitute char/line) to work in NORMAL mode, matching vim behavior</p>
|
||||
<p>• [VSCode] Updated the Remote Control banner to describe what it does</p>
|
||||
<p>• Claude in Chrome: hardened file-upload path validation</p>
|
||||
<p>• Claude in Chrome: save_to_disk on screenshot actions now writes the image to disk and returns the path; previously it did nothing</p>
|
||||
<p>• Fixed a prompt-caching regression on Bedrock, Vertex, Mantle, and Foundry that billed the trailing system context block as fresh input tokens on every request.</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.210</id>
|
||||
<title>Claude Code v2.1.210</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.210"/>
|
||||
<updated>2026-07-14T23:45:19Z</updated>
|
||||
<content type="html"><p>• Added a live elapsed-time counter to the collapsed tool summary line so long-running tool calls visibly tick instead of looking stuck</p>
|
||||
<p>• Added a startup warning for Write(path), NotebookEdit(path), and Glob(path) permission rules — use Edit(path) or Read(path) instead</p>
|
||||
<p>• Fixed isolation: 'worktree' subagents being able to run git-mutating commands against the main repo checkout instead of their own isolated worktree</p>
|
||||
<p>• Fixed the ultracode keyword opt-in firing on non-human-originated input such as webhook payloads and relayed PR comments</p>
|
||||
<p>• Fixed a rendered text fragment leaking into crash telemetry when a UI component returned content outside a styled text element</p>
|
||||
<p>• Fixed paste markers leaking into external editors opened from Claude Code, which could appear as stray È/É characters around pasted text</p>
|
||||
<p>• Fixed claude attach sometimes failing with "job not found" or "agent is still starting" errors during session transitions — attach now waits for the daemon to settle, and terminal resizes during a slow attach are applied once it completes</p>
|
||||
<p>• Fixed a session crash when a tool's result renderer returned a numeric bigint value or plain text instead of a UI element</p>
|
||||
<p>• Fixed a hook callback timeout being misreported to the model as a user rejection, which made unattended sessions stop and wait</p>
|
||||
<p>• Fixed Claude assuming a cd took effect after its command was moved to the background; the tool result now states the working directory is unchanged</p>
|
||||
<p>• Fixed plugin-provided MCP servers being torn down when MCP servers are re-synced mid-session</p>
|
||||
<p>• Fixed plan approvals without edits being labeled "(edited by user)" and overwriting the plan file with a stale snapshot</p>
|
||||
<p>• Fixed /doctor skipping its auto-mode-default proposal on Bedrock, Vertex, and Foundry, where auto mode no longer needs an opt-in</p>
|
||||
<p>• Fixed Grep content mode claiming "No matches found" when paginating past the end of results</p>
|
||||
<p>• Fixed unmatched $1/$2 positional placeholders in skills and commands being silently stripped; they are now preserved verbatim</p>
|
||||
<p>• Fixed plugin cache writes leaving temp files behind on failure and failing on locked-file renames on Windows and network filesystems</p>
|
||||
<p>• Fixed background workers crash-looping when a client resets its connection to the background service</p>
|
||||
<p>• Fixed claude agents --effort ultracode not reaching dispatched sessions; the value was silently dropped</p>
|
||||
<p>• Fixed pressing ← to open the agents view dropping the task tracker when returning to the session</p>
|
||||
<p>• Fixed the agents dashboard retaining pasted images from abandoned reply drafts after their session was deleted</p>
|
||||
<p>• Fixed killed background sessions leaving a permanent git worktree lock behind; the periodic sweep now releases locks whose owning process is gone</p>
|
||||
<p>• Fixed SDK MCP servers registered via an initialize control request waiting until the next turn to start connecting</p>
|
||||
<p>• Fixed returning to the agents view from a session leaving overlapping ghost frames with CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1</p>
|
||||
<p>• Fixed late-appearing .claude/* symlinks not being reconciled into the sandbox deny-write list</p>
|
||||
<p>• Hardened the Agent tool against indirect prompt injection via content a subagent read</p>
|
||||
<p>• Improved the Bash/PowerShell tool message when a command hits its timeout and is auto-backgrounded, so the model can distinguish a hang from an explicit background request</p>
|
||||
<p>• Improved auto mode: the permission classifier now defaults to Sonnet 5 for external sessions, validated on the session's first request and pinned for the session</p>
|
||||
<p>• Improved the bundled dataviz skill's chart color validation with perceptual OKLab color difference and recalibrated color-blindness thresholds</p>
|
||||
<p>• Memory writes that leave a MEMORY.md index over its read limit now produce an explicit error instead of silent truncation</p>
|
||||
<p>• Screen reader mode now announces permission mode changes aloud when cycling modes with Shift+Tab</p>
|
||||
<p>• The agents footer hint now shows how many background agents are waiting on your input, with a brief color emphasis when the count changes</p>
|
||||
<p>• Agent view: the session you pressed ← from stays visibly marked even after mouse hover or arrow keys move the selection</p>
|
||||
<p>• Fable temporarily shows as unavailable in the advisor picker while a server-side issue causing Fable advisor failures is fixed</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.209</id>
|
||||
<title>Claude Code v2.1.209</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.209"/>
|
||||
<updated>2026-07-14T06:36:21Z</updated>
|
||||
<content type="html"><p>• Fixed /model and other dialogs being blocked in claude agents background sessions (reverts an overly broad guard)</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.208</id>
|
||||
<title>Claude Code v2.1.208</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.208"/>
|
||||
<updated>2026-07-14T01:10:34Z</updated>
|
||||
<content type="html"><p>• Added screen reader mode: opt-in plain-text rendering for screen reader users. Run claude --ax-screen-reader, set CLAUDE_AX_SCREEN_READER=1, or add "axScreenReader": true to settings.</p>
|
||||
<p>• Added vimInsertModeRemaps setting: map two-key insert-mode sequences like jj to Escape in vim mode</p>
|
||||
<p>• Added CLAUDE_CODE_PROCESS_WRAPPER: agent view and the background service now honor a corporate launcher by running every Claude Code self-spawn through a required wrapper executable</p>
|
||||
<p>• Added mouse-click support for multi-select menus and "Other" input rows in fullscreen mode</p>
|
||||
<p>• Changed the Fable 5 usage-credits consent prompt to start with the decline option focused</p>
|
||||
<p>• Fixed fast mode staying off after switching back to a model that supports it — it now restores automatically when enabled in settings</p>
|
||||
<p>• Fixed replies typed to a background agent being lost when delivery fails — the text is now saved and delivered when the session restarts</p>
|
||||
<p>• Fixed background-session attach failing permanently ("Couldn't start the background daemon") after an update replaced the binary a running claude agents process was launched from</p>
|
||||
<p>• Fixed the context window (and auto-compact indicator) briefly resetting to 200k after the CLI auto-updates, causing a false "100% context used" when resuming long-context sessions</p>
|
||||
<p>• Fixed supervised and background sessions crashing when a server closed an HTTP/2 connection with a GOAWAY while requests were in flight</p>
|
||||
<p>• Fixed truncated stream-json/JSON output and missing result message when piping large responses from claude -p</p>
|
||||
<p>• Fixed CLAUDE_CODE_MAX_OUTPUT_TOKENS and similar env vars silently using the mantissa of scientific-notation values (1e6 became 1)</p>
|
||||
<p>• Fixed very large markdown tables stalling rendering or using excessive memory; tables over 200 rows show the first 200 with a "… N more rows" notice</p>
|
||||
<p>• Fixed the Edit tool failing on files modified after reading when the target text still matches uniquely</p>
|
||||
<p>• Fixed Read reporting empty files as "shorter than offset", Grep silently returning "No files found" for invalid regex patterns, Grep count mode under-reporting totals when paginated, and Glob crashing with an unclear error when the pattern, path, or working directory contained a null byte</p>
|
||||
<p>• Fixed apiKeyHelper script failures being hidden behind a generic 401 after ~10 silent retries; the script's own error is now shown within 3 attempts</p>
|
||||
<p>• Fixed Bedrock streaming requests failing with a misleading "Truncated event message received" when a gateway transforms the response — the error now names the content-type and points at the proxy</p>
|
||||
<p>• Fixed /upgrade showing a login flow instead of the upgrade URL when the browser fails to open</p>
|
||||
<p>• Fixed stream-json input killing the session on blank CRLF or whitespace-only lines from Windows-style SDK hosts</p>
|
||||
<p>• Fixed headless stream-json sessions hanging permanently when a control_request carried a non-string set_model payload; the CLI now answers with an error response</p>
|
||||
<p>• Fixed repeated "No completion record was found" notices on session resume — orphaned background tasks now collapse into a single summary</p>
|
||||
<p>• Fixed Remote Control clients attaching to a terminal-hosted session not seeing background agents and workflow progress until a task started or stopped</p>
|
||||
<p>• Fixed the Agent tool launching with no tools when a subagent's tools list resolves to nothing — it now returns a clear error naming the unrecognized entries</p>
|
||||
<p>• Fixed /usage showing stale cached bars over fresher data, and /mcp not reclassifying placeholder servers after config edits</p>
|
||||
<p>• Fixed "Change directory" in SDK hosts (e.g. Claude Desktop) failing with "A turn is in progress" on idle sessions that have a running background task</p>
|
||||
<p>• Fixed the workflow save dialog showing ~/.claude/workflows/ instead of the CLAUDE_CONFIG_DIR location for user-scope saves</p>
|
||||
<p>• Fixed /release-notes adding the viewed notes to the model's context — "Show all" previously injected the entire changelog into every subsequent request</p>
|
||||
<p>• Fixed a memory leak in the agent view where pasted images were retained for the screen's lifetime after sending peek replies</p>
|
||||
<p>• Fixed SDK sessions losing agents defined via the initialize request when a plugin refresh ran before the client attached</p>
|
||||
<p>• Fixed several memory leaks in long sessions: MCP stdio server stderr accumulating up to 64 MB per server, LSP documents staying open indefinitely (now LRU with 50-doc cap), async hook output retained after backgrounding, and unbounded growth in headless/SDK sessions from large tool-result payloads</p>
|
||||
<p>• Fixed a memory blowup when reading files with extremely long single lines using offset/limit — the read now returns a clean error instead of loading the whole line</p>
|
||||
<p>• Fixed multi-second per-turn slowdowns in sessions with many permission deny/ask rules — rule matchers are now compiled once and cached</p>
|
||||
<p>• Improved input responsiveness while agent task lists update — task updates no longer re-render the entire UI</p>
|
||||
<p>• Reduced per-tool-call CPU overhead in print/SDK sessions with many MCP tools by caching tool-pool assembly (up to 7x faster tool rounds at high tool counts)</p>
|
||||
<p>• Reduced memory usage by bounding the file edit read cache to 16 MB instead of pinning up to 1,000 full files</p>
|
||||
<p>• Reduced session transcript size (up to 79x in edit-heavy sessions) and bounded checkpoint disk usage by pruning superseded file-history backups</p>
|
||||
<p>• Reduced memory usage when resuming sessions with background agents or forks spawned from large conversations</p>
|
||||
<p>• Completed background agents now stay listed in /tasks until cleanup instead of vanishing the moment they finish</p>
|
||||
<p>• Attaching to a stopped background agent now shows its transcript immediately while the session warms up, instead of a blank "Session is starting" screen</p>
|
||||
<p>• Background sessions: an older daemon no longer silently restarts workers spawned by a newer version onto the older binary</p>
|
||||
<p>• Agent view: Ctrl+X now deletes renamed-branch worktrees, never destroys unpushed commits, keeps the session row when a worktree is kept, and reused worktree names reset to the current base</p>
|
||||
<p>• Catastrophic removals (e.g. rm -rf ~) in commands containing $(…)/backticks/&lt;(…) now prompt in --dangerously-skip-permissions and auto mode, matching the plain form</p>
|
||||
<p>• /install-github-app and the /mcp settings menu no longer open in background sessions</p>
|
||||
<p>• MCP servers configured with an empty URL now show as "not configured" in /mcp instead of a config error</p>
|
||||
<p>• /usage now shows your last-known usage bars with an "as of" note when the usage endpoint is rate-limited, instead of an error screen</p>
|
||||
<p>• Fixed Bedrock auth failing with "Session token not found or invalid" for AWS SSO profiles whose sso_region differs from the Bedrock region (2.1.207 regression)</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.207</id>
|
||||
<title>Claude Code v2.1.207</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.207"/>
|
||||
<updated>2026-07-11T00:52:04Z</updated>
|
||||
<content type="html"><p>• Auto mode is now available without CLAUDE_CODE_ENABLE_AUTO_MODE opt-in on Bedrock, Vertex AI, and Foundry; disable via disableAutoMode in settings</p>
|
||||
<p>• Fixed the terminal freezing and keystrokes lagging while streaming responses containing very long lists, tables, paragraphs, or code blocks</p>
|
||||
<p>• Fixed remote managed settings from a non-interactive run (claude -p, the SDK) being permanently recorded as consented without ever showing the security consent dialog</p>
|
||||
<p>• Fixed spurious prompt-injection warnings triggered by benign system-generated conversation updates</p>
|
||||
<p>• Fixed the auto-updater overwriting a custom launcher script or symlink at ~/.local/bin/claude on every release; /doctor now reports an externally managed launcher</p>
|
||||
<p>• Fixed compound commands with cd prompting for permission when the only output redirect was to /dev/null</p>
|
||||
<p>• Fixed the transcript jumping above the start of the answer when a response finishes streaming</p>
|
||||
<p>• Fixed extensions.worktreeConfig being left in the repo's .git/config (breaking go-git tools like tea) after the last worktree.sparsePaths worktree was removed</p>
|
||||
<p>• Fixed malformed bracket patterns in rules globs, skill paths, .ignore, and .worktreeinclude breaking file reads, file suggestions, and worktree creation</p>
|
||||
<p>• Fixed a crash loop in agent teams where a malformed teammate mailbox message caused repeated errors every second until the mailbox file was manually deleted</p>
|
||||
<p>• Fixed background sessions auto-named by accepting a plan not showing that name on their agent-view row</p>
|
||||
<p>• Fixed background sessions that entered a git worktree resuming blank after a cold reopen from the agent list</p>
|
||||
<p>• Fixed Remote Control task status updates being lost when the connection recovered from a network interruption or credential refresh</p>
|
||||
<p>• Fixed Remote Control sessions hosted by the desktop app not showing background agent and workflow progress on mobile and web</p>
|
||||
<p>• Fixed Deep research runs labeling every Fetch-phase agent "unknown" — chips now show the source hostname</p>
|
||||
<p>• Fixed Bedrock repeatedly requesting fresh AWS SSO credentials from IAM Identity Center on every API request</p>
|
||||
<p>• Improved agent view: pasting the same text again now expands the collapsed [Pasted text #N] placeholder instead of adding a second one</p>
|
||||
<p>• Improved agent view: blocked session peeks now lead with the question and show a worded staleness clock (waiting 3m) instead of the same timestamp twice</p>
|
||||
<p>• Changed Bedrock, Vertex, and Claude Platform on AWS to default to Claude Opus 4.8</p>
|
||||
<p>• Changed auto mode to no longer read autoMode from .claude/settings.local.json (repo-resident); use ~/.claude/settings.json instead</p>
|
||||
<p>• Fixed an indefinite hang on Windows when AWS credential resolution stalls (e.g. a stuck credential_process): the 60-second stall guard now fires instead of waiting forever.</p>
|
||||
<p>• Plugin hooks/monitors/MCP headersHelper: ${user_config.*} in shell-form commands is now rejected (shell-injection fix). Hooks: use exec form (args array) or $CLAUDE_PLUGIN_OPTION_&lt;KEY&gt;; monitors and headersHelper: read the value inside the script (config file or the server's env block).</p>
|
||||
<p>• Plugin option values (pluginConfigs) are no longer read from project-level .claude/settings.json; only user, --settings, and managed settings are honored</p>
|
||||
<p>• Fixed /usage-credits amount inputs silently stripping malformed values (e.g. a pasted timestamp) to digits; malformed amounts are now rejected with an error, and amounts over $1,000 require a typed confirmation</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.206</id>
|
||||
<title>Claude Code v2.1.206</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.206"/>
|
||||
<updated>2026-07-09T23:34:23Z</updated>
|
||||
<content type="html"><p>• Added directory path suggestions to /cd, matching /add-dir behavior</p>
|
||||
<p>• Added a /doctor check that proposes trimming checked-in CLAUDE.md files by cutting content Claude could derive from the codebase</p>
|
||||
<p>• /commit-push-pr now auto-allows git push to the repo's configured push remote (remote.pushDefault, or the sole remote when only one is configured) in addition to origin</p>
|
||||
<p>• Gateway: /login now supports Anthropic-operated public gateway endpoints</p>
|
||||
<p>• EnterWorktree now asks for confirmation before entering a git worktree outside the project's .claude/worktrees/ directory</p>
|
||||
<p>• Background agents now upgrade to a new version in the background right after a Claude Code update, instead of paying a slow stale-session upgrade when you attach</p>
|
||||
<p>• Fixed an expired login failing every model with a misleading "There's an issue with the selected model" error instead of prompting to run /login</p>
|
||||
<p>• Fixed claude --resume and --continue not responding to keyboard input on startup</p>
|
||||
<p>• Fixed MCP servers configured via --mcp-config or .mcp.json ignoring a per-server request_timeout_ms, which caused long-running MCP tool calls to time out at the 60s default in fresh sessions</p>
|
||||
<p>• Fixed CLAUDE_CODE_EXTRA_BODY being silently ignored by claude agents / --bg background workers; the shell-exported override now follows the dispatching session</p>
|
||||
<p>• Fixed OAuth MCP servers requiring manual re-authentication after a single failed token refresh</p>
|
||||
<p>• Fixed --permission-prompt-tool pointing at an MCP server crashing with "MCP tool not found" on cold start before the server finishes connecting</p>
|
||||
<p>• Fixed /model picker rows printing a price for a different model than the row named, and stopped quoting first-party list prices on providers that don't bill them</p>
|
||||
<p>• Fixed server-provided model rows being misplaced in the /model picker when an entitlement or allowlist restriction drops the row they were positioned against</p>
|
||||
<p>• Fixed desktop sessions getting stuck showing "running" after a slash command was sent mid-turn</p>
|
||||
<p>• Fixed keyboard input being ignored in the agents view when a setup prompt appeared before a bare claude --resume on Windows</p>
|
||||
<p>• Fixed claude rm leaving the removed job in the daemon roster, causing the row to reappear in claude agents</p>
|
||||
<p>• Fixed /remote-control showing "Unknown command" when logged out — it now explains how to sign in</p>
|
||||
<p>• Fixed left arrow not stepping back out of a phase or agent in the workflow detail view</p>
|
||||
<p>• Fixed /status listing the same broken-install warning twice</p>
|
||||
<p>• Fixed false "disused plugin" tips and skewed disuse telemetry for LSP plugins</p>
|
||||
<p>• Fixed /doctor's update check to compare Homebrew installs against their cask's channel instead of the settings channel</p>
|
||||
<p>• Fixed the fullscreen jump-to-bottom pill suggesting Ctrl+End on macOS, not showing rebound chords, and wrapping over the transcript</p>
|
||||
<p>• Bedrock: fixed a multi-minute startup hang when using an awsCredentialExport helper on networks with restricted egress</p>
|
||||
<p>• Improved /code-review findings quality on claude-opus-4-8 across all effort levels</p>
|
||||
<p>• Improved agents view: status column now uses full terminal width instead of truncating at 64 characters</p>
|
||||
<p>• Changed agents view: Ctrl+X now permanently removes a completed session, and sessions no longer render twice; deleted background jobs stay deleted</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.205</id>
|
||||
<title>Claude Code v2.1.205</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.205"/>
|
||||
<updated>2026-07-08T21:21:58Z</updated>
|
||||
<content type="html"><p>• Added an auto mode rule that blocks tampering with session transcript files</p>
|
||||
<p>• Fixed --json-schema silently producing unstructured output when the schema was invalid, and schemas using the format keyword being rejected</p>
|
||||
<p>• Fixed a message sent while Claude was working being silently lost when the turn ended at the --max-turns limit</p>
|
||||
<p>• Fixed Windows worktree removal deleting files outside the worktree when an NTFS junction or directory symlink existed inside it</p>
|
||||
<p>• Fixed background agents staying shown as "failed" or "completed" in the agent list after being resumed with SendMessage</p>
|
||||
<p>• Fixed background jobs flipping from "needs input" back to "working" in the agent list when the agent's turn contained no readable text</p>
|
||||
<p>• Fixed claude attach erroring when a background agent was mid-upgrade restart instead of waiting for it to come back</p>
|
||||
<p>• Fixed session-to-PR linking missing a PR created in a Bash call whose output exceeded the 30K inline limit</p>
|
||||
<p>• Fixed claude mcp add-from-claude-desktop getting stuck when a server name contains unsupported characters; invalid names are now reported and remaining servers still import</p>
|
||||
<p>• Fixed a plugin LSP server that fails to initialize preventing a valid LSP server from another plugin handling the same file extension</p>
|
||||
<p>• Fixed a Windows crash when the directory Claude was launched from is deleted, locked, or unmounted while a command is running</p>
|
||||
<p>• Fixed a crash when a file watcher was closed while a directory scan was still in flight</p>
|
||||
<p>• Fixed project verify skills being rewritten on every session instead of only when a documented command changed</p>
|
||||
<p>• Fixed the agent view rendering one line too high and clipping its header when the job list slightly overflowed the screen</p>
|
||||
<p>• Fixed background tasks in the web and mobile Remote Control panels showing stale "Running" status by forwarding full task state on every membership change</p>
|
||||
<p>• Improved auto mode to ask before running rm -rf on a variable it can't resolve from context</p>
|
||||
<p>• Auto-update binary downloads now stream to disk instead of buffering in memory, cutting the updater's peak memory usage by roughly 400 MB</p>
|
||||
<p>• Background task notifications now explicitly state that no human input has occurred, preventing fabricated in-transcript approvals from being acted on</p>
|
||||
<p>• Improved agent view: sessions that edit, merge, comment on, or push to an existing PR now link it in claude agents</p>
|
||||
<p>• Improved agent view: rows now show a colored state word and a classifier-written headline instead of raw tool call text, and the peek opens with full status including the exact ask for blocked sessions</p>
|
||||
<p>• /doctor is now a full setup checkup that can diagnose and fix issues; /checkup is its alias</p>
|
||||
<p>• Reserved the "Claude Browser" MCP server name (alongside "Claude Preview") ahead of the Claude Desktop pane rename; user-configured MCP servers can no longer register under either name</p>
|
||||
<p>• Fixed Cowork VM-mode local-agent sessions failing to start with "Not logged in · Please run /login" on CLI 2.1.203+</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.204</id>
|
||||
<title>Claude Code v2.1.204</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.204"/>
|
||||
<updated>2026-07-08T00:27:43Z</updated>
|
||||
<content type="html"><p>• Fixed hook events not streaming during SessionStart hooks in headless sessions, which could cause remote workers to be idle-reaped mid-hook</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.203</id>
|
||||
<title>Claude Code v2.1.203</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.203"/>
|
||||
<updated>2026-07-07T21:06:03Z</updated>
|
||||
<content type="html"><p>• Added a warning when your login is about to expire, so you can re-authenticate before background sessions are interrupted</p>
|
||||
<p>• Added a grey ⏸ badge to the footer when in manual permission mode, making the active mode always visible</p>
|
||||
<p>• Added the session's additional working directories to MCP roots/list, with notifications/roots/list_changed sent when the set changes</p>
|
||||
<p>• Fixed opening or switching background agent sessions on macOS stalling for 15–20 seconds due to a false low-memory detection (regression in 2.1.196)</p>
|
||||
<p>• Fixed background sessions becoming permanently unresponsive to attach, replies, and stop when the daemon's session token went stale — the session now recovers automatically</p>
|
||||
<p>• Fixed returning to claude agents silently stopping running subagents and re-running the prompt from scratch — their work now carries over</p>
|
||||
<p>• Fixed a memory and per-turn CPU regression in interactive sessions: the context-usage indicator no longer re-analyzes the entire transcript after every turn</p>
|
||||
<p>• Fixed background agents inheriting a stale PATH from the daemon instead of the dispatching shell, causing missing tools on Windows</p>
|
||||
<p>• Fixed background and agent-view sessions dropping a shell-exported ANTHROPIC_BASE_URL, which sent API keys to the default endpoint and failed with 401</p>
|
||||
<p>• Fixed Bash failing with "argument list too long" in repos with many git worktrees</p>
|
||||
<p>• Fixed worktree-isolated subagents sometimes running shell commands in the parent checkout instead of their own worktree</p>
|
||||
<p>• Fixed worktree creation rejecting nested repositories in multi-repo workspaces, leaving background sessions unable to isolate and edit</p>
|
||||
<p>• Fixed background agents crash-looping when their working directory was deleted, replaced by a file, or became an invalid path — they now fail once with a clear error</p>
|
||||
<p>• Fixed a background daemon auto-upgrade failure silently killing all running background sessions</p>
|
||||
<p>• Fixed TaskStop and TaskOutput failing to find background agents spawned by another agent — errors now list running agents by id and description</p>
|
||||
<p>• Fixed the claude agents composer discarding your typed message when a slash command isn't available there</p>
|
||||
<p>• Fixed the agent list crashing when opening a stopped session whose conversation was already open in another session</p>
|
||||
<p>• Fixed background sessions showing "Needs input" in the agent list after the question was already answered</p>
|
||||
<p>• Fixed background agent startup failures showing only "exit_with_message" instead of the actual error</p>
|
||||
<p>• Fixed background sessions ignoring effortLevel changes in settings.json when forked through the daemon</p>
|
||||
<p>• Fixed attached background sessions ignoring CLAUDE_CODE_DISABLE_MOUSE and CLAUDE_CODE_DISABLE_MOUSE_CLICKS opt-outs</p>
|
||||
<p>• Fixed /exit incorrectly warning about running background agents after all named agents had completed</p>
|
||||
<p>• Fixed background sessions started from a non-git directory unable to edit files when a WorktreeCreate hook was configured</p>
|
||||
<p>• Fixed the @ directory picker in claude agents not showing registered git worktrees</p>
|
||||
<p>• Fixed background task output on Windows being permanently replaced by an empty file after /clear</p>
|
||||
<p>• Fixed content jumping when scrolling up through long transcript history</p>
|
||||
<p>• Fixed the terminal flickering and jumping while typing in bash mode when a shell-history suggestion was shown</p>
|
||||
<p>• Fixed literal ^[[I / ^[[O escape codes being printed when reattaching to a background session</p>
|
||||
<p>• Fixed LSP-only plugins being incorrectly flagged for disuse when their language servers deliver diagnostics or answer navigation requests</p>
|
||||
<p>• Improved responsiveness while long responses stream: live-preview updates no longer re-render the whole screen</p>
|
||||
<p>• Improved subagent behavior: agents are now less likely to re-delegate their entire task to another subagent</p>
|
||||
<p>• Reduced binary size by ~7 MB and startup memory by ~7 MB by loading a large bundled dependency lazily instead of inlining it</p>
|
||||
<p>• Changed left arrow to no longer close the background tasks, diff, and workflow detail views — press Esc instead</p>
|
||||
<p>• Changed the empty claude agents view to always show the organized sections (Needs input / Working / Completed) with descriptions</p>
|
||||
<p>• Removed the startup "claude command missing or broken" warnings — they now appear in /doctor and /status instead</p>
|
||||
<p>• Removed a redundant navigation hint from the claude agents footer</p>
|
||||
<p>• [VSCode] Added a Settings toggle for "Enable Remote Control for all sessions"</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.202</id>
|
||||
<title>Claude Code v2.1.202</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.202"/>
|
||||
<updated>2026-07-06T22:51:10Z</updated>
|
||||
<content type="html"><p>• Added a "Dynamic workflow size" setting in /config for controlling how large Claude generally makes dynamic workflows (small/medium/large agent counts) — an advisory guideline, not an enforced cap</p>
|
||||
<p>• Added workflow.run_id and workflow.name OpenTelemetry attributes to telemetry emitted by workflow-spawned agents, so a workflow run's activity can be reconstructed from OTel data</p>
|
||||
<p>• Fixed a crash in the inline Ctrl+R history search when accepting or cancelling while the search was still scanning the history file</p>
|
||||
<p>• Fixed /rename on background sessions being reverted when the job restarts, which broke addressing the session by its new name</p>
|
||||
<p>• Fixed transient mTLS handshake failures when settings were re-applied during an in-place client certificate rotation</p>
|
||||
<p>• Fixed commands sent from Remote Control (mobile/web) into an interactive session failing with "Unknown command"</p>
|
||||
<p>• Fixed images and files sent from the Remote Control mobile or web app without a caption being silently dropped</p>
|
||||
<p>• Fixed the sign-in URL printed by claude auth login and claude mcp login --no-browser not being reliably clickable when it wraps over SSH — it is now emitted as a single hyperlink</p>
|
||||
<p>• Fixed opening a chat from claude agents sometimes failing with "currently running as a background agent" followed by a worker crash/respawn loop</p>
|
||||
<p>• Fixed workflow scripts with unicode quote escapes in strings being corrupted before parsing; workflow parse errors now show the offending line instead of always blaming TypeScript</p>
|
||||
<p>• Fixed voice dictation retrying in an unbounded loop when the microphone or audio recorder fails — repeated capture failures now pause voice input</p>
|
||||
<p>• Fixed /remote-control sessions showing the wrong permission mode in the mobile and web apps</p>
|
||||
<p>• Fixed resuming a session by name, or opening the resume picker, taking minutes and using a large amount of memory in repositories with many git worktrees</p>
|
||||
<p>• Fixed installer and updater downloads failing immediately with "aborted" when a proxy or network drops the connection mid-download — transient connection drops now retry</p>
|
||||
<p>• Fixed re-invoking an already-loaded skill appending a duplicate copy of its instructions to context</p>
|
||||
<p>• Improved /workflows agent list layout: wider titles, a dedicated time column, shorter model names, and no per-row tool-call counts</p>
|
||||
<p>• Improved MCP error messages: clearer error when a server config has url but no type, suggesting "type": "http" instead of the misleading "command: expected string"</p>
|
||||
<p>• Changed /review &lt;pr&gt; back to a fast single-pass review; use /code-review &lt;level&gt; &lt;pr#&gt; for the multi-agent review at a chosen effort level</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.201</id>
|
||||
<title>Claude Code v2.1.201</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.201"/>
|
||||
<updated>2026-07-03T23:50:29Z</updated>
|
||||
<content type="html"><p>• Claude Sonnet 5 sessions no longer use the mid-conversation system role for harness reminders</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.200</id>
|
||||
<title>Claude Code v2.1.200</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.200"/>
|
||||
<updated>2026-07-03T16:52:26Z</updated>
|
||||
<content type="html"><p>• Changed AskUserQuestion dialogs to no longer auto-continue by default; opt into an idle timeout via /config</p>
|
||||
<p>• Changed the "default" permission mode to "Manual" across the CLI, --help, VS Code, and JetBrains; --permission-mode manual and "defaultMode": "manual" are accepted alongside default</p>
|
||||
<p>• Fixed a crash at startup when disabledMcpServers or enabledMcpServers in .claude.json is set to a non-array value</p>
|
||||
<p>• Fixed background sessions silently stopping mid-turn after sleep/wake or when reopening a stalled session</p>
|
||||
<p>• Fixed background sessions re-running a turn cancelled with Esc after a stall respawn</p>
|
||||
<p>• Fixed background agents never starting again after a crash left a stale daemon.lock whose PID the OS reused</p>
|
||||
<p>• Fixed background-agent daemon handover so a reinstalled older build can no longer take over the daemon; build recency is now judged by the version's embedded build timestamp</p>
|
||||
<p>• Fixed background-agent roster issues: transient corruption permanently disabling orphan cleanup, older binaries not preserving fields written by newer versions, and socket auth tokens being stripped during daemon restarts</p>
|
||||
<p>• Fixed subagents cut off by a rate limit before producing any text output returning an empty result instead of failing cleanly</p>
|
||||
<p>• Fixed control bytes from background-agent output reaching the terminal in the agent view</p>
|
||||
<p>• Fixed claude agents --plugin-dir &lt;dir&gt; not showing the plugin's agents and skills in the agent view when the flag is placed after agents</p>
|
||||
<p>• Fixed project-scoped plugins not loading correctly from git worktrees of the same repository</p>
|
||||
<p>• Fixed /mcp server list not tracking focus for screen readers and magnifiers</p>
|
||||
<p>• Fixed voice dictation showing a misleading "Voice connection failed" message when a recording captures no audio</p>
|
||||
<p>• Fixed rendering flicker under tmux 3.4+ by enabling synchronized terminal output</p>
|
||||
<p>• Improved screen-reader output: decorative glyphs are now hidden, transcript symbols read as short labels, and nested tables read as Header: value. lines</p>
|
||||
<p>• Improved the install script to explain when installation is killed by the system running out of memory</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.199</id>
|
||||
<title>Claude Code v2.1.199</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.199"/>
|
||||
<updated>2026-07-02T23:35:12Z</updated>
|
||||
<content type="html"><p>• Stacked slash-skill invocations like /skill-a /skill-b do XYZ now load all leading skills (up to 5), not just the first</p>
|
||||
<p>• Fixed SSL certificate errors (TLS-inspecting proxies, missing NODE_EXTRA_CA_CERTS, expired certs) burning retries before showing actionable guidance — they now fail immediately with the fix hint</p>
|
||||
<p>• Fixed streaming responses being discarded when the API emits a mid-stream overloaded/server error after partial output — the partial is now kept with an incomplete-response notice</p>
|
||||
<p>• Fixed subagents cut off by a rate limit or server error silently failing instead of returning their partial work to the parent</p>
|
||||
<p>• Fixed subagents reporting API errors (e.g. usage limit reached) as successful results — the error is now reported to the parent agent</p>
|
||||
<p>• Fixed the background-agent daemon on Linux killing itself and every running agent every ~50 seconds after an unclean shutdown left a corrupted worker record</p>
|
||||
<p>• Fixed background agents failing to cold-start over SSH on macOS with "Could not switch to audit session" (regression in 2.1.196)</p>
|
||||
<p>• Fixed claude stop being silently undone when it raced a background-agent respawn — the respawn now honors the stop</p>
|
||||
<p>• Fixed background job progress indicators stalling for minutes while the job ran long commands</p>
|
||||
<p>• Fixed background sessions on memory-starved machines showing a generic error — they now indicate low memory and suggest freeing resources</p>
|
||||
<p>• Fixed remote sessions briefly flapping between Working and Idle in the agent view when a background agent completes</p>
|
||||
<p>• Fixed idle subagents vanishing from the agent panel while other subagents were still working; surplus idle agents now collapse into an expandable summary row</p>
|
||||
<p>• Fixed typing /model or /fast while viewing a subagent silently opening the lead's model picker — a notice now explains the command applies to the lead</p>
|
||||
<p>• Fixed SessionStart, Setup, and SubagentStart hooks silently hiding stderr when exiting with code 2 — the error is now shown in the transcript</p>
|
||||
<p>• Fixed claude --dangerously-skip-permissions daemon &lt;subcommand&gt; being treated as a chat prompt instead of running the subcommand</p>
|
||||
<p>• Fixed SendMessage silently misrouting when a re-spawned agent reuses a previous agent's name — the tool now detects the mismatch and asks the caller to retarget</p>
|
||||
<p>• Fixed opening or resuming a session with no new messages needlessly growing the transcript file</p>
|
||||
<p>• Fixed backgrounding a session with ← or /background dropping its /color from the agent view row</p>
|
||||
<p>• Fixed resetting a corrupted config file from the startup recovery dialog destroying it unrecoverably — it now backs up the file first</p>
|
||||
<p>• Fixed Claude in Chrome repeatedly opening the reconnect page when sessions run from different builds or config directories</p>
|
||||
<p>• Fixed plan mode not prompting for state-changing browser tool calls; read-only browser_batch calls are now correctly auto-allowed</p>
|
||||
<p>• Transient server rate-limit errors (429s unrelated to your usage limit) are now retried automatically with backoff for subscribers instead of failing the turn</p>
|
||||
<p>• CLAUDE_CODE_RETRY_WATCHDOG now raises the default retry count for non-capacity transient errors to 300 and lifts the cap of 15 on CLAUDE_CODE_MAX_RETRIES</p>
|
||||
<p>• claude agents session rows now show pull-request links as bare #N without the redundant "PR" label</p></content>
|
||||
</entry>
|
||||
</feed>
|
||||
+20
-46
@@ -10,50 +10,21 @@ Learn more in the [official plugins documentation](https://docs.claude.com/en/do
|
||||
|
||||
## Plugins in This Directory
|
||||
|
||||
### [agent-sdk-dev](./agent-sdk-dev/)
|
||||
|
||||
**Claude Agent SDK Development Plugin**
|
||||
|
||||
Streamlines the development of Claude Agent SDK applications with scaffolding commands and verification agents.
|
||||
|
||||
- **Command**: `/new-sdk-app` - Interactive setup for new Agent SDK projects
|
||||
- **Agents**: `agent-sdk-verifier-py` and `agent-sdk-verifier-ts` - Validate SDK applications against best practices
|
||||
- **Use case**: Creating and verifying Claude Agent SDK applications in Python or TypeScript
|
||||
|
||||
### [commit-commands](./commit-commands/)
|
||||
|
||||
**Git Workflow Automation Plugin**
|
||||
|
||||
Simplifies common git operations with streamlined commands for committing, pushing, and creating pull requests.
|
||||
|
||||
- **Commands**:
|
||||
- `/commit` - Create a git commit with appropriate message
|
||||
- `/commit-push-pr` - Commit, push, and create a PR in one command
|
||||
- `/clean_gone` - Clean up stale local branches marked as [gone]
|
||||
- **Use case**: Faster git workflows with less context switching
|
||||
|
||||
### [code-review](./code-review/)
|
||||
|
||||
**Automated Pull Request Code Review Plugin**
|
||||
|
||||
Provides automated code review for pull requests using multiple specialized agents with confidence-based scoring to filter false positives.
|
||||
|
||||
- **Command**:
|
||||
- `/code-review` - Automated PR review workflow
|
||||
- **Use case**: Automated code review on pull requests with high-confidence issue detection (threshold ≥80)
|
||||
|
||||
### [feature-dev](./feature-dev/)
|
||||
|
||||
**Comprehensive Feature Development Workflow Plugin**
|
||||
|
||||
Provides a structured 7-phase approach to feature development with specialized agents for exploration, architecture, and review.
|
||||
|
||||
- **Command**: `/feature-dev` - Guided feature development workflow
|
||||
- **Agents**:
|
||||
- `code-explorer` - Deeply analyzes existing codebase features
|
||||
- `code-architect` - Designs feature architectures and implementation blueprints
|
||||
- `code-reviewer` - Reviews code for bugs, quality issues, and project conventions
|
||||
- **Use case**: Building new features with systematic codebase understanding and quality assurance
|
||||
| Name | Description | Contents |
|
||||
|------|-------------|----------|
|
||||
| [agent-sdk-dev](./agent-sdk-dev/) | Development kit for working with the Claude Agent SDK | **Command:** `/new-sdk-app` - Interactive setup for new Agent SDK projects<br>**Agents:** `agent-sdk-verifier-py`, `agent-sdk-verifier-ts` - Validate SDK applications against best practices |
|
||||
| [claude-opus-4-5-migration](./claude-opus-4-5-migration/) | Migrate code and prompts from Sonnet 4.x and Opus 4.1 to Opus 4.5 | **Skill:** `claude-opus-4-5-migration` - Automated migration of model strings, beta headers, and prompt adjustments |
|
||||
| [code-review](./code-review/) | Automated PR code review using multiple specialized agents with confidence-based scoring to filter false positives | **Command:** `/code-review` - Automated PR review workflow<br>**Agents:** 5 parallel Sonnet agents for CLAUDE.md compliance, bug detection, historical context, PR history, and code comments |
|
||||
| [commit-commands](./commit-commands/) | Git workflow automation for committing, pushing, and creating pull requests | **Commands:** `/commit`, `/commit-push-pr`, `/clean_gone` - Streamlined git operations |
|
||||
| [explanatory-output-style](./explanatory-output-style/) | Adds educational insights about implementation choices and codebase patterns (mimics the deprecated Explanatory output style) | **Hook:** SessionStart - Injects educational context at the start of each session |
|
||||
| [feature-dev](./feature-dev/) | Comprehensive feature development workflow with a structured 7-phase approach | **Command:** `/feature-dev` - Guided feature development workflow<br>**Agents:** `code-explorer`, `code-architect`, `code-reviewer` - For codebase analysis, architecture design, and quality review |
|
||||
| [frontend-design](./frontend-design/) | Create distinctive, production-grade frontend interfaces that avoid generic AI aesthetics | **Skill:** `frontend-design` - Auto-invoked for frontend work, providing guidance on bold design choices, typography, animations, and visual details |
|
||||
| [hookify](./hookify/) | Easily create custom hooks to prevent unwanted behaviors by analyzing conversation patterns or explicit instructions | **Commands:** `/hookify`, `/hookify:list`, `/hookify:configure`, `/hookify:help`<br>**Agent:** `conversation-analyzer` - Analyzes conversations for problematic behaviors<br>**Skill:** `writing-rules` - Guidance on hookify rule syntax |
|
||||
| [learning-output-style](./learning-output-style/) | Interactive learning mode that requests meaningful code contributions at decision points (mimics the unshipped Learning output style) | **Hook:** SessionStart - Encourages users to write meaningful code (5-10 lines) at decision points while receiving educational insights |
|
||||
| [plugin-dev](./plugin-dev/) | Comprehensive toolkit for developing Claude Code plugins with 7 expert skills and AI-assisted creation | **Command:** `/plugin-dev:create-plugin` - 8-phase guided workflow for building plugins<br>**Agents:** `agent-creator`, `plugin-validator`, `skill-reviewer`<br>**Skills:** Hook development, MCP integration, plugin structure, settings, commands, agents, and skill development |
|
||||
| [pr-review-toolkit](./pr-review-toolkit/) | Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification | **Command:** `/pr-review-toolkit:review-pr` - Run with optional review aspects (comments, tests, errors, types, code, simplify, all)<br>**Agents:** `comment-analyzer`, `pr-test-analyzer`, `silent-failure-hunter`, `type-design-analyzer`, `code-reviewer`, `code-simplifier` |
|
||||
| [ralph-wiggum](./ralph-wiggum/) | Interactive self-referential AI loops for iterative development. Claude works on the same task repeatedly until completion | **Commands:** `/ralph-loop`, `/cancel-ralph` - Start/stop autonomous iteration loops<br>**Hook:** Stop - Intercepts exit attempts to continue iteration |
|
||||
| [security-guidance](./security-guidance/) | Security reminder hook that warns about potential security issues when editing files | **Hook:** PreToolUse - Monitors 9 security patterns including command injection, XSS, eval usage, dangerous HTML, pickle deserialization, and os.system calls |
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -81,8 +52,11 @@ Each plugin follows the standard Claude Code plugin structure:
|
||||
plugin-name/
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json # Plugin metadata
|
||||
├── commands/ # Slash commands (optional)
|
||||
├── agents/ # Specialized agents (optional)
|
||||
├── commands/ # Slash commands (optional)
|
||||
├── agents/ # Specialized agents (optional)
|
||||
├── skills/ # Agent Skills (optional)
|
||||
├── hooks/ # Event handlers (optional)
|
||||
├── .mcp.json # External tool configuration (optional)
|
||||
└── README.md # Plugin documentation
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "claude-opus-4-5-migration",
|
||||
"version": "1.0.0",
|
||||
"description": "Migrate your code and prompts from Sonnet 4.x and Opus 4.1 to Opus 4.5.",
|
||||
"author": {
|
||||
"name": "William Hu",
|
||||
"email": "whu@anthropic.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Claude Opus 4.5 Migration Plugin
|
||||
|
||||
Migrate your code and prompts from Sonnet 4.x and Opus 4.1 to Opus 4.5.
|
||||
|
||||
## Overview
|
||||
|
||||
This skill updates your code and prompts to be compatible with Opus 4.5. It automates the migration process, handling model strings, beta headers, and other configuration details. If you run into any issues with Opus 4.5 after migration, you can continue using this skill to adjust your prompts.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
"Migrate my codebase to Opus 4.5"
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
Refer to our [prompting guide](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-4-best-practices) for best practices on prompting Claude models.
|
||||
|
||||
## Authors
|
||||
|
||||
William Hu (whu@anthropic.com)
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: claude-opus-4-5-migration
|
||||
description: Migrate prompts and code from Claude Sonnet 4.0, Sonnet 4.5, or Opus 4.1 to Opus 4.5. Use when the user wants to update their codebase, prompts, or API calls to use Opus 4.5. Handles model string updates and prompt adjustments for known Opus 4.5 behavioral differences. Does NOT migrate Haiku 4.5.
|
||||
---
|
||||
|
||||
# Opus 4.5 Migration Guide
|
||||
|
||||
One-shot migration from Sonnet 4.0, Sonnet 4.5, or Opus 4.1 to Opus 4.5.
|
||||
|
||||
## Migration Workflow
|
||||
|
||||
1. Search codebase for model strings and API calls
|
||||
2. Update model strings to Opus 4.5 (see platform-specific strings below)
|
||||
3. Remove unsupported beta headers
|
||||
4. Add effort parameter set to `"high"` (see `references/effort.md`)
|
||||
5. Summarize all changes made
|
||||
6. Tell the user: "If you encounter any issues with Opus 4.5, let me know and I can help adjust your prompts."
|
||||
|
||||
## Model String Updates
|
||||
|
||||
Identify which platform the codebase uses, then replace model strings accordingly.
|
||||
|
||||
### Unsupported Beta Headers
|
||||
|
||||
Remove the `context-1m-2025-08-07` beta header if present—it is not yet supported with Opus 4.5. Leave a comment noting this:
|
||||
|
||||
```python
|
||||
# Note: 1M context beta (context-1m-2025-08-07) not yet supported with Opus 4.5
|
||||
```
|
||||
|
||||
### Target Model Strings (Opus 4.5)
|
||||
|
||||
| Platform | Opus 4.5 Model String |
|
||||
|----------|----------------------|
|
||||
| Anthropic API (1P) | `claude-opus-4-5-20251101` |
|
||||
| AWS Bedrock | `anthropic.claude-opus-4-5-20251101-v1:0` |
|
||||
| Google Vertex AI | `claude-opus-4-5@20251101` |
|
||||
| Azure AI Foundry | `claude-opus-4-5-20251101` |
|
||||
|
||||
### Source Model Strings to Replace
|
||||
|
||||
| Source Model | Anthropic API (1P) | AWS Bedrock | Google Vertex AI |
|
||||
|--------------|-------------------|-------------|------------------|
|
||||
| Sonnet 4.0 | `claude-sonnet-4-20250514` | `anthropic.claude-sonnet-4-20250514-v1:0` | `claude-sonnet-4@20250514` |
|
||||
| Sonnet 4.5 | `claude-sonnet-4-5-20250929` | `anthropic.claude-sonnet-4-5-20250929-v1:0` | `claude-sonnet-4-5@20250929` |
|
||||
| Opus 4.1 | `claude-opus-4-1-20250422` | `anthropic.claude-opus-4-1-20250422-v1:0` | `claude-opus-4-1@20250422` |
|
||||
|
||||
**Do NOT migrate**: Any Haiku models (e.g., `claude-haiku-4-5-20251001`).
|
||||
|
||||
## Prompt Adjustments
|
||||
|
||||
Opus 4.5 has known behavioral differences from previous models. **Only apply these fixes if the user explicitly requests them or reports a specific issue.** By default, just update model strings.
|
||||
|
||||
**Integration guidelines**: When adding snippets, don't just append them to prompts. Integrate them thoughtfully:
|
||||
- Use XML tags (e.g., `<code_guidelines>`, `<tool_usage>`) to organize additions
|
||||
- Match the style and structure of the existing prompt
|
||||
- Place snippets in logical locations (e.g., coding guidelines near other coding instructions)
|
||||
- If the prompt already uses XML tags, add new content within appropriate existing tags or create consistent new ones
|
||||
|
||||
### 1. Tool Overtriggering
|
||||
|
||||
Opus 4.5 is more responsive to system prompts. Aggressive language that prevented undertriggering on previous models may now cause overtriggering.
|
||||
|
||||
**Apply if**: User reports tools being called too frequently or unnecessarily.
|
||||
|
||||
**Find and soften**:
|
||||
- `CRITICAL:` → remove or soften
|
||||
- `You MUST...` → `You should...`
|
||||
- `ALWAYS do X` → `Do X`
|
||||
- `NEVER skip...` → `Don't skip...`
|
||||
- `REQUIRED` → remove or soften
|
||||
|
||||
Only apply to tool-triggering instructions. Leave other uses of emphasis alone.
|
||||
|
||||
### 2. Over-Engineering Prevention
|
||||
|
||||
Opus 4.5 tends to create extra files, add unnecessary abstractions, or build unrequested flexibility.
|
||||
|
||||
**Apply if**: User reports unwanted files, excessive abstraction, or unrequested features. Add the snippet from `references/prompt-snippets.md`.
|
||||
|
||||
### 3. Code Exploration
|
||||
|
||||
Opus 4.5 can be overly conservative about exploring code, proposing solutions without reading files.
|
||||
|
||||
**Apply if**: User reports the model proposing fixes without inspecting relevant code. Add the snippet from `references/prompt-snippets.md`.
|
||||
|
||||
### 4. Frontend Design
|
||||
|
||||
**Apply if**: User requests improved frontend design quality or reports generic-looking outputs.
|
||||
|
||||
Add the frontend aesthetics snippet from `references/prompt-snippets.md`.
|
||||
|
||||
### 5. Thinking Sensitivity
|
||||
|
||||
When extended thinking is not enabled (the default), Opus 4.5 is particularly sensitive to the word "think" and its variants. Extended thinking is enabled only if the API request contains a `thinking` parameter.
|
||||
|
||||
**Apply if**: User reports issues related to "thinking" while extended thinking is not enabled (no `thinking` parameter in request).
|
||||
|
||||
Replace "think" with alternatives like "consider," "believe," or "evaluate."
|
||||
|
||||
## Reference
|
||||
|
||||
See `references/prompt-snippets.md` for the full text of each snippet to add.
|
||||
|
||||
See `references/effort.md` for configuring the effort parameter (only if user requests it).
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# Effort Parameter (Beta)
|
||||
|
||||
**Add effort set to `"high"` during migration.** This is the default configuration for best performance with Opus 4.5.
|
||||
|
||||
## Overview
|
||||
|
||||
Effort controls how eagerly Claude spends tokens. It affects all tokens: thinking, text responses, and function calls.
|
||||
|
||||
| Effort | Use Case |
|
||||
|--------|----------|
|
||||
| `high` | Best performance, deep reasoning (default) |
|
||||
| `medium` | Balance of cost/latency vs. performance |
|
||||
| `low` | Simple, high-volume queries; significant token savings |
|
||||
|
||||
## Implementation
|
||||
|
||||
Requires beta flag `effort-2025-11-24` in API calls.
|
||||
|
||||
**Python SDK:**
|
||||
```python
|
||||
response = client.messages.create(
|
||||
model="claude-opus-4-5-20251101",
|
||||
max_tokens=1024,
|
||||
betas=["effort-2025-11-24"],
|
||||
output_config={
|
||||
"effort": "high" # or "medium" or "low"
|
||||
},
|
||||
messages=[...]
|
||||
)
|
||||
```
|
||||
|
||||
**TypeScript SDK:**
|
||||
```typescript
|
||||
const response = await client.messages.create({
|
||||
model: "claude-opus-4-5-20251101",
|
||||
max_tokens: 1024,
|
||||
betas: ["effort-2025-11-24"],
|
||||
output_config: {
|
||||
effort: "high" // or "medium" or "low"
|
||||
},
|
||||
messages: [...]
|
||||
});
|
||||
```
|
||||
|
||||
**Raw API:**
|
||||
```json
|
||||
{
|
||||
"model": "claude-opus-4-5-20251101",
|
||||
"max_tokens": 1024,
|
||||
"anthropic-beta": "effort-2025-11-24",
|
||||
"output_config": {
|
||||
"effort": "high"
|
||||
},
|
||||
"messages": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## Effort vs. Thinking Budget
|
||||
|
||||
Effort is independent of thinking budget:
|
||||
|
||||
- High effort + no thinking = more tokens, but no thinking tokens
|
||||
- High effort + 32k thinking = more tokens, but thinking capped at 32k
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. First determine effort level, then set thinking budget
|
||||
2. Best performance: high effort + high thinking budget
|
||||
3. Cost/latency optimization: medium effort
|
||||
4. Simple high-volume queries: low effort
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
# Prompt Snippets for Opus 4.5
|
||||
|
||||
Only apply these snippets if the user explicitly requests them or reports a specific issue. By default, the migration should only update model strings.
|
||||
|
||||
## 1. Tool Overtriggering
|
||||
|
||||
**Problem**: Prompts designed to reduce undertriggering on previous models may cause Opus 4.5 to overtrigger.
|
||||
|
||||
**When to add**: User reports tools being called too frequently or unnecessarily.
|
||||
|
||||
**Solution**: Replace aggressive language with normal phrasing.
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| `CRITICAL: You MUST use this tool when...` | `Use this tool when...` |
|
||||
| `ALWAYS call the search function before...` | `Call the search function before...` |
|
||||
| `You are REQUIRED to...` | `You should...` |
|
||||
| `NEVER skip this step` | `Don't skip this step` |
|
||||
|
||||
## 2. Over-Engineering Prevention
|
||||
|
||||
**Problem**: Opus 4.5 may create extra files, add unnecessary abstractions, or build unrequested flexibility.
|
||||
|
||||
**When to add**: User reports unwanted files, excessive abstraction, or unrequested features.
|
||||
|
||||
**Snippet to add to system prompt**:
|
||||
|
||||
```
|
||||
- Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.
|
||||
- Don't add features, refactor code, or make "improvements" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability.
|
||||
- Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use backwards-compatibility shims when you can just change the code.
|
||||
- Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is the minimum needed for the current task. Reuse existing abstractions where possible and follow the DRY principle.
|
||||
```
|
||||
|
||||
## 3. Code Exploration
|
||||
|
||||
**Problem**: Opus 4.5 may propose solutions without reading code or make assumptions about unread files.
|
||||
|
||||
**When to add**: User reports the model proposing fixes without inspecting relevant code.
|
||||
|
||||
**Snippet to add to system prompt**:
|
||||
|
||||
```
|
||||
ALWAYS read and understand relevant files before proposing code edits. Do not speculate about code you have not inspected. If the user references a specific file/path, you MUST open and inspect it before explaining or proposing fixes. Be rigorous and persistent in searching code for key facts. Thoroughly review the style, conventions, and abstractions of the codebase before implementing new features or abstractions.
|
||||
```
|
||||
|
||||
## 4. Frontend Design Quality
|
||||
|
||||
**Problem**: Default frontend outputs may look generic ("AI slop" aesthetic).
|
||||
|
||||
**When to add**: User requests improved frontend design quality or reports generic-looking outputs.
|
||||
|
||||
**Snippet to add to system prompt**:
|
||||
|
||||
```xml
|
||||
<frontend_aesthetics>
|
||||
You tend to converge toward generic, "on distribution" outputs. In frontend design, this creates what users call the "AI slop" aesthetic. Avoid this: make creative, distinctive frontends that surprise and delight.
|
||||
|
||||
Focus on:
|
||||
- Typography: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics.
|
||||
- Color & Theme: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Draw from IDE themes and cultural aesthetics for inspiration.
|
||||
- Motion: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions.
|
||||
- Backgrounds: Create atmosphere and depth rather than defaulting to solid colors. Layer CSS gradients, use geometric patterns, or add contextual effects that match the overall aesthetic.
|
||||
|
||||
Avoid generic AI-generated aesthetics:
|
||||
- Overused font families (Inter, Roboto, Arial, system fonts)
|
||||
- Clichéd color schemes (particularly purple gradients on white backgrounds)
|
||||
- Predictable layouts and component patterns
|
||||
- Cookie-cutter design that lacks context-specific character
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. Vary between light and dark themes, different fonts, different aesthetics. You still tend to converge on common choices (Space Grotesk, for example) across generations. Avoid this: it is critical that you think outside the box!
|
||||
</frontend_aesthetics>
|
||||
```
|
||||
|
||||
## 5. Thinking Sensitivity
|
||||
|
||||
**Problem**: When extended thinking is not enabled (the default), Opus 4.5 is particularly sensitive to the word "think" and its variants.
|
||||
|
||||
Extended thinking is not enabled by default. It is only enabled if the API request contains a `thinking` parameter:
|
||||
```json
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"budget_tokens": 10000
|
||||
}
|
||||
```
|
||||
|
||||
**When to apply**: User reports issues related to "thinking" while extended thinking is not enabled (no `thinking` parameter in their request).
|
||||
|
||||
**Solution**: Replace "think" with alternative words.
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| `think about` | `consider` |
|
||||
| `think through` | `evaluate` |
|
||||
| `I think` | `I believe` |
|
||||
| `think carefully` | `consider carefully` |
|
||||
| `thinking` | `reasoning` / `considering` |
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
1. **Integrate thoughtfully** - Don't just append snippets; weave them into the existing prompt structure
|
||||
2. **Use XML tags** - Wrap additions in descriptive tags (e.g., `<coding_guidelines>`, `<tool_behavior>`) that match or complement existing prompt structure
|
||||
3. **Match prompt style** - If the prompt is concise, trim the snippet; if verbose, keep full detail
|
||||
4. **Place logically** - Put coding snippets near other coding instructions, tool guidance near tool definitions, etc.
|
||||
5. **Preserve existing content** - Insert snippets without removing functional content
|
||||
6. **Summarize changes** - After migration, list all model string updates and prompt modifications made
|
||||
@@ -22,23 +22,29 @@ Performs automated code review on a pull request using multiple specialized agen
|
||||
- **Agent #4**: Analyze git blame/history for context-based issues
|
||||
5. Scores each issue 0-100 for confidence level
|
||||
6. Filters out issues below 80 confidence threshold
|
||||
7. Posts review comment with high-confidence issues only
|
||||
7. Outputs review (to terminal by default, or as PR comment with `--comment` flag)
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
/code-review
|
||||
/code-review [--comment]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--comment`: Post the review as a comment on the pull request (default: outputs to terminal only)
|
||||
|
||||
**Example workflow:**
|
||||
```bash
|
||||
# On a PR branch, run:
|
||||
# On a PR branch, run locally (outputs to terminal):
|
||||
/code-review
|
||||
|
||||
# Post review as PR comment:
|
||||
/code-review --comment
|
||||
|
||||
# Claude will:
|
||||
# - Launch 4 review agents in parallel
|
||||
# - Score each issue for confidence
|
||||
# - Post comment with issues ≥80 confidence
|
||||
# - Skip posting if no high-confidence issues found
|
||||
# - Output issues ≥80 confidence (to terminal or PR depending on flag)
|
||||
# - Skip if no high-confidence issues found
|
||||
```
|
||||
|
||||
**Features:**
|
||||
@@ -114,17 +120,23 @@ This plugin is included in the Claude Code repository. The command is automatica
|
||||
### Standard PR review workflow:
|
||||
```bash
|
||||
# Create PR with changes
|
||||
# Run local review (outputs to terminal)
|
||||
/code-review
|
||||
|
||||
# Review the automated feedback
|
||||
# Make any necessary fixes
|
||||
|
||||
# Optionally post as PR comment
|
||||
/code-review --comment
|
||||
|
||||
# Merge when ready
|
||||
```
|
||||
|
||||
### As part of CI/CD:
|
||||
```bash
|
||||
# Trigger on PR creation or update
|
||||
# Automatically posts review comments
|
||||
# Use --comment flag to post review comments
|
||||
/code-review --comment
|
||||
# Skip if review already exists
|
||||
```
|
||||
|
||||
|
||||
@@ -1,83 +1,108 @@
|
||||
---
|
||||
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*), Bash(gh pr review:*), Bash(gh pr list:*)
|
||||
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*), Bash(gh pr list:*), mcp__github_inline_comment__create_inline_comment
|
||||
description: Code review a pull request
|
||||
---
|
||||
|
||||
Provide a code review for the given pull request.
|
||||
|
||||
**Agent assumptions (applies to all agents and subagents):**
|
||||
- All tools are functional and will work without error. Do not test tools or make exploratory calls. Make sure this is clear to every subagent that is launched.
|
||||
- Only call a tool if it is required to complete the task. Every tool call should have a clear purpose.
|
||||
|
||||
To do this, follow these steps precisely:
|
||||
|
||||
1. Use an agent to check if the pull request (a) is closed, (b) is a draft, (c) does not need a code review (eg. because it is an automated pull request, or is very simple and obviously ok), or (d) already has a code review from you from earlier. If so, do not proceed.
|
||||
2. Use another agent to give you a list of file paths to (but not the contents of) any relevant CLAUDE.md files from the codebase: the root CLAUDE.md file (if one exists), as well as any CLAUDE.md files in the directories whose files the pull request modified
|
||||
3. Use an agent to view the pull request, and ask the agent to return a summary of the change
|
||||
4. Then, launch 4 parallel agents to independently code review the change. The agents should do the following, then return a list of issues and the reason each issue was flagged (eg. CLAUDE.md adherence, bug, historical git context, etc.):
|
||||
a. Agents #1 and #2: Independently audit the changes to make sure they compily with the CLAUDE.md
|
||||
b. Agent #3: Read the file changes in the pull request, then do a shallow scan for obvious bugs. Avoid reading extra context beyond the changes, focusing just on the changes themselves. Focus on large bugs, and avoid small issues and nitpicks. Ignore likely false positives.
|
||||
c. Agent #5: Read the git blame and history of the code modified, to identify any bugs in light of that historical context
|
||||
5. For each issue found in #4, launch a parallel agent that takes the PR, issue description, and list of CLAUDE.md files (from step 2), and returns a score to indicate the agent's level of confidence for whether the issue is real or false positive. To do that, the agent should score each issue on a scale from 0-100, indicating its level of confidence. For issues that were flagged due to CLAUDE.md instructions, the agent should double check that the CLAUDE.md actually calls out that issue specifically. The scale is (give this rubric to the agent verbatim):
|
||||
a. 0: Not confident at all. This is a false positive that doesn't stand up to light scrutiny, or is a pre-existing issue.
|
||||
b. 25: Somewhat confident. This might be a real issue, but may also be a false positive. The agent wasn't able to verify that it's a real issue. If the issue is stylistic, it is one that was not explicitly called out in the relevant CLAUDE.md.
|
||||
c. 50: Moderately confident. The agent was able to verify this is a real issue, but it might be a nitpick or not happen very often in practice. Relative to the rest of the PR, it's not very important.
|
||||
d. 75: Highly confident. The agent double checked the issue, and verified that it is very likely it is a real issue that will be hit in practice. The existing approach in the PR is insufficient. The issue is very important and will directly impact the code's functionality, or it is an issue that is directly mentioned in the relevant CLAUDE.md.
|
||||
e. 100: Absolutely certain. The agent double checked the issue, and confirmed that it is definitely a real issue, that will happen frequently in practice. The evidence directly confirms this.
|
||||
6. Filter out any issues with a score less than 80. If there are no issues that meet this criteria, do not proceed.
|
||||
7. Finally, comment back on the pull request with a list of issues you found. When writing your comment, keep in mind to:
|
||||
a. Keep your output brief
|
||||
b. Avoid emojis
|
||||
c. Link and cite relevant code, files, and URLs
|
||||
1. Launch a haiku agent to check if any of the following are true:
|
||||
- The pull request is closed
|
||||
- The pull request is a draft
|
||||
- The pull request does not need code review (e.g. automated PR, trivial change that is obviously correct)
|
||||
- Claude has already commented on this PR (check `gh pr view <PR> --comments` for comments left by claude)
|
||||
|
||||
Examples of false positives, for steps 4 and 5:
|
||||
If any condition is true, stop and do not proceed.
|
||||
|
||||
Note: Still review Claude generated PR's.
|
||||
|
||||
2. Launch a haiku agent to return a list of file paths (not their contents) for all relevant CLAUDE.md files including:
|
||||
- The root CLAUDE.md file, if it exists
|
||||
- Any CLAUDE.md files in directories containing files modified by the pull request
|
||||
|
||||
3. Launch a sonnet agent to view the pull request and return a summary of the changes
|
||||
|
||||
4. Launch 4 agents in parallel to independently review the changes. Each agent should return the list of issues, where each issue includes a description and the reason it was flagged (e.g. "CLAUDE.md adherence", "bug"). The agents should do the following:
|
||||
|
||||
Agents 1 + 2: CLAUDE.md compliance sonnet agents
|
||||
Audit changes for CLAUDE.md compliance in parallel. Note: When evaluating CLAUDE.md compliance for a file, you should only consider CLAUDE.md files that share a file path with the file or parents.
|
||||
|
||||
Agent 3: Opus bug agent (parallel subagent with agent 4)
|
||||
Scan for obvious bugs. Focus only on the diff itself without reading extra context. Flag only significant bugs; ignore nitpicks and likely false positives. Do not flag issues that you cannot validate without looking at context outside of the git diff.
|
||||
|
||||
Agent 4: Opus bug agent (parallel subagent with agent 3)
|
||||
Look for problems that exist in the introduced code. This could be security issues, incorrect logic, etc. Only look for issues that fall within the changed code.
|
||||
|
||||
**CRITICAL: We only want HIGH SIGNAL issues.** Flag issues where:
|
||||
- The code will fail to compile or parse (syntax errors, type errors, missing imports, unresolved references)
|
||||
- The code will definitely produce wrong results regardless of inputs (clear logic errors)
|
||||
- Clear, unambiguous CLAUDE.md violations where you can quote the exact rule being broken
|
||||
|
||||
Do NOT flag:
|
||||
- Code style or quality concerns
|
||||
- Potential issues that depend on specific inputs or state
|
||||
- Subjective suggestions or improvements
|
||||
|
||||
If you are not certain an issue is real, do not flag it. False positives erode trust and waste reviewer time.
|
||||
|
||||
In addition to the above, each subagent should be told the PR title and description. This will help provide context regarding the author's intent.
|
||||
|
||||
5. For each issue found in the previous step by agents 3 and 4, launch parallel subagents to validate the issue. These subagents should get the PR title and description along with a description of the issue. The agent's job is to review the issue to validate that the stated issue is truly an issue with high confidence. For example, if an issue such as "variable is not defined" was flagged, the subagent's job would be to validate that is actually true in the code. Another example would be CLAUDE.md issues. The agent should validate that the CLAUDE.md rule that was violated is scoped for this file and is actually violated. Use Opus subagents for bugs and logic issues, and sonnet agents for CLAUDE.md violations.
|
||||
|
||||
6. Filter out any issues that were not validated in step 5. This step will give us our list of high signal issues for our review.
|
||||
|
||||
7. Output a summary of the review findings to the terminal:
|
||||
- If issues were found, list each issue with a brief description.
|
||||
- If no issues were found, state: "No issues found. Checked for bugs and CLAUDE.md compliance."
|
||||
|
||||
If `--comment` argument was NOT provided, stop here. Do not post any GitHub comments.
|
||||
|
||||
If `--comment` argument IS provided and NO issues were found, post a summary comment using `gh pr comment` and stop.
|
||||
|
||||
If `--comment` argument IS provided and issues were found, continue to step 8.
|
||||
|
||||
8. Create a list of all comments that you plan on leaving. This is only for you to make sure you are comfortable with the comments. Do not post this list anywhere.
|
||||
|
||||
9. Post inline comments for each issue using `mcp__github_inline_comment__create_inline_comment` with `confirmed: true`. For each comment:
|
||||
- Provide a brief description of the issue
|
||||
- For small, self-contained fixes, include a committable suggestion block
|
||||
- For larger fixes (6+ lines, structural changes, or changes spanning multiple locations), describe the issue and suggested fix without a suggestion block
|
||||
- Never post a committable suggestion UNLESS committing the suggestion fixes the issue entirely. If follow up steps are required, do not leave a committable suggestion.
|
||||
|
||||
**IMPORTANT: Only post ONE comment per unique issue. Do not post duplicate comments.**
|
||||
|
||||
Use this list when evaluating issues in Steps 4 and 5 (these are false positives, do NOT flag):
|
||||
|
||||
- Pre-existing issues
|
||||
- Something that looks like a bug but is not actually a bug
|
||||
- Pedantic nitpicks that a senior engineer wouldn't call out
|
||||
- Issues that a linter will catch (no need to run the linter to verify)
|
||||
- General code quality issues (eg. lack of test coverage, general security issues), unless explicitly required in CLAUDE.md
|
||||
- Issues that are called out in CLAUDE.md, but explicitly silenced in the code (eg. due to a lint ignore comment)
|
||||
- Something that appears to be a bug but is actually correct
|
||||
- Pedantic nitpicks that a senior engineer would not flag
|
||||
- Issues that a linter will catch (do not run the linter to verify)
|
||||
- General code quality concerns (e.g., lack of test coverage, general security issues) unless explicitly required in CLAUDE.md
|
||||
- Issues mentioned in CLAUDE.md but explicitly silenced in the code (e.g., via a lint ignore comment)
|
||||
|
||||
Notes:
|
||||
|
||||
- Use `gh` to interact with Github (eg. to fetch a pull request, or to create inline comments), rather than web fetch
|
||||
- Make a todo list first
|
||||
- You must cite and link each bug (eg. if referring to a CLAUDE.md, you must link it)
|
||||
- For your comment, follow the following format precisely (assuming for this example that you found 3 issues):
|
||||
- Use gh CLI to interact with GitHub (e.g., fetch pull requests, create comments). Do not use web fetch.
|
||||
- Create a todo list before starting.
|
||||
- You must cite and link each issue in inline comments (e.g., if referring to a CLAUDE.md, include a link to it).
|
||||
- If no issues are found and `--comment` argument is provided, post a comment with the following format:
|
||||
|
||||
---
|
||||
|
||||
## Code review
|
||||
|
||||
Found 3 issues:
|
||||
|
||||
1. <brief description of bug> (CLAUDE.md says "<...>")
|
||||
|
||||
<link to file and line with full sha1 + line range for context, eg. https://github.com/anthropics/claude-code/blob/1d54823877c4de72b2316a64032a54afc404e619/README.md#L13-L17>
|
||||
|
||||
2. <brief description of bug> (some/other/CLAUDE.md says "<...>")
|
||||
|
||||
<link to file and line with full sha1 + line range for context>
|
||||
|
||||
3. <brief description of bug> (bug due to <file and code snippet>)
|
||||
|
||||
<link to file and line with full sha1 + line range for context>
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
<sub>- If this code review was useful, please react with 👍. Otherwise, react with 👎.</sub>
|
||||
|
||||
---
|
||||
|
||||
- Or, if you found no issues:
|
||||
|
||||
---
|
||||
|
||||
## Auto code review
|
||||
|
||||
No issues found. Checked for bugs and CLAUDE.md compliance.
|
||||
|
||||
## 🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
---
|
||||
|
||||
- When linking to code, follow the following format precisely, otherwise the Markdown preview won't render correctly: https://github.com/anthropics/claude-cli-internal/blob/c21d3c10bc8e898b7ac1a2d745bdc9bc4e423afe/package.json#L10-L15
|
||||
- When linking to code in inline comments, follow the following format precisely, otherwise the Markdown preview won't render correctly: https://github.com/anthropics/claude-code/blob/c21d3c10bc8e898b7ac1a2d745bdc9bc4e423afe/package.json#L10-L15
|
||||
- Requires full git sha
|
||||
- You must provide the full sha. Commands like `https://github.com/owner/repo/blob/$(git rev-parse HEAD)/foo/bar` will not work, since your comment will be directly rendered in Markdown.
|
||||
- Repo name must match the repo you're code reviewing
|
||||
- # sign after the file name
|
||||
- Line range format is L[start]-L[end]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Output the explanatory mode instructions as additionalContext
|
||||
# This mimics the deprecated Explanatory output style
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "frontend-design",
|
||||
"version": "1.1.0",
|
||||
"description": "Frontend design skill for UI/UX implementation",
|
||||
"author": {
|
||||
"name": "Prithvi Rajasekaran, Alexander Bricken",
|
||||
"email": "prithvi@anthropic.com, alexander@anthropic.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# Frontend Design Plugin
|
||||
|
||||
Generates distinctive, production-grade frontend interfaces that avoid generic AI aesthetics.
|
||||
|
||||
## What It Does
|
||||
|
||||
Claude automatically uses this skill for frontend work. Creates production-ready code with:
|
||||
|
||||
- Bold aesthetic choices
|
||||
- Distinctive typography and color palettes
|
||||
- High-impact animations and visual details
|
||||
- Context-aware implementation
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
"Create a dashboard for a music streaming app"
|
||||
"Build a landing page for an AI security startup"
|
||||
"Design a settings panel with dark mode"
|
||||
```
|
||||
|
||||
Claude will choose a clear aesthetic direction and implement production code with meticulous attention to detail.
|
||||
|
||||
## Learn More
|
||||
|
||||
See the [Frontend Aesthetics Cookbook](https://github.com/anthropics/claude-cookbooks/blob/main/coding/prompting_for_frontend_aesthetics.ipynb) for detailed guidance on prompting for high-quality frontend design.
|
||||
|
||||
## Authors
|
||||
|
||||
Prithvi Rajasekaran (prithvi@anthropic.com)
|
||||
Alexander Bricken (alexander@anthropic.com)
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Frontend Design
|
||||
|
||||
Approach this as the design lead at a small studio known for giving every client a visual identity that could not be mistaken for anyone else's. This client has already rejected proposals that felt templated, and is paying for a distinctive point of view: make deliberate, opinionated choices about palette, typography, and layout that are specific to this brief, and take one real aesthetic risk you can justify.
|
||||
|
||||
## Ground it in the subject
|
||||
|
||||
If the brief does not pin down what the product or subject is, pin it yourself before designing: name one concrete subject, its audience, and the page's single job, and state your choice. If there's any information in your memory about the human's preferences, context about what they're building, or designs you've made before – use that as a hint. The subject's own world, its materials, instruments, artifacts, and vernacular, is where distinctive choices come from. Build with the brief's real content and subject matter throughout.
|
||||
|
||||
## Design principles
|
||||
|
||||
For web designs, the hero is a thesis. Open with the most characteristic thing in the subject's world, in whatever form makes sense for it: a headline, an image, an animation, a live demo, an interactive moment. Be deliberate with your choice: a big number with a small label, supporting stats, and a gradient accent is the template answer, only use if that's truly the best option.
|
||||
|
||||
Typography carries the personality of the page. Pair the display and body faces deliberately, not the same families you would reach for on any other project, and set a clear type scale with intentional weights, widths, and spacing. Make the type treatment itself a memorable part of the design, not a neutral delivery vehicle for the content.
|
||||
|
||||
Structure is information. Structural devices, numbering, eyebrows, dividers, labels, should encode something true about the content, not decorate it. Many generic designs use numbered markers (01 / 02 / 03), but that's only appropriate if the content actually is a sequence - like a real process or a typed timeline where order carries information the reader needs. Question if choices like numbered markers actually make sense before incorporating them.
|
||||
|
||||
Leverage motion deliberately. Think about where and if animation can serve the subject: a page-load sequence, a scroll-triggered reveal, hover micro-interactions, ambient atmosphere. An orchestrated moment usually lands harder than scattered effects; choose what the direction calls for. However, sometimes less is more, and extra animation contributes to the feeling that the design is AI-generated.
|
||||
|
||||
Match complexity to the vision. Maximalist directions need elaborate execution; minimal directions need precision in spacing, type, and detail. Elegance is executing the chosen vision well.
|
||||
|
||||
Consider written content carefully. Often a design brief may not contain real content, and it's up to you to come up with copy. Copy can make a design feel as templated as the design itself. See the below section on writing for more guidance.
|
||||
|
||||
## Process: brainstorm, explore, plan, critique, build, critique again
|
||||
|
||||
For calibration: AI-generated design right now clusters around three looks: (1) a warm cream background (near #F4F1EA) with a high-contrast serif display and a terracotta accent; (2) a near-black background with a single bright acid-green or vermilion accent; (3) a broadsheet-style layout with hairline rules, zero border-radius, and dense newspaper-like columns. All three are legitimate for some briefs, but they are defaults rather than choices, and they appear regardless of subject. Where the brief pins down a visual direction, follow it exactly — the brief's own words always win, including when it asks for one of these looks. Where it leaves an axis free, don't spend that freedom on one of these defaults. Just like a human designer who's hired, there's often a careful balance between doing what you're good at and taking each project as a chance to experiment and learn.
|
||||
|
||||
Work in two passes. First, brainstorm a short design plan based on the human's design brief: create a compact token system with color, type, layout, and signature. Color: describe the palette as 4–6 named hex values. Type: the typefaces for 2+ roles (a characterful display face that's used with restraint, a complementary body face, and a utility face for captions or data if needed). Layout: a layout concept, using one-sentence prose descriptions and ASCII wireframes to ideate and compare. Signature: the single unique element this page will be remembered by that embodies the brief in an appropriate way.
|
||||
|
||||
Then review that plan against the brief before building: if any part of it reads like the generic default you would produce for any similar page (work through a similar prompt to see if you arrive somewhere similar) rather than a choice made for this specific brief — revise that part, say what you changed and why. Only after you've confirmed the relative uniqueness of your design plan should you start to write the code, following the revised plan exactly and deriving every color and type decision from it.
|
||||
|
||||
When writing the code, be careful of structuring your CSS selector specificities. It's easy to generate CSS classes that cancel each other out (especially with a type-based selector like .section and a element-based selector like .cta). This can happen often with paddings/margins between sections.
|
||||
|
||||
Try to do a lot of this planning and iteration in your thinking, and only show ideas to the user when you have higher confidence it'll delight them.
|
||||
|
||||
## Restraint and self-critique
|
||||
|
||||
Spend your boldness in one place. Let the signature element be the one memorable thing, keep everything around it quiet and disciplined, and cut any decoration that does not serve the brief. Not taking a risk can be a risk itself! Build to a quality floor without announcing it: responsive down to mobile, visible keyboard focus, reduced motion respected. Critique your own work as you build, taking screenshots if your environment supports it – a picture is worth 1000 tokens. Consider Chanel's advice: before leaving the house, take a look in the mirror and remove one accessory. Human creators have memory and always try to do something new, so if you have a space to quickly jot down notes about what you've tried, it can help you in future passes.
|
||||
|
||||
## More on writing in design
|
||||
|
||||
Words appear in a design for one reason: to make it easier to understand, and therefore easier to use. They are design material, not decoration. Bring the same intentionality to copy that you would bring to spacing and color. Before writing anything, ask what the design needs to say, and how it can best be said to help the person navigate the experience.
|
||||
|
||||
Write from the end user's side of the screen. Name things by what people control and recognize, never by how the system is built. A person manages notifications, not webhook config. Describe what something does in plain terms rather than selling it. Being specific is always better than being clever.
|
||||
|
||||
Use active voice as default. A control should say exactly what happens when it's used: "Save changes," not "Submit." An action keeps the same name through the whole flow, so the button that says "Publish" produces a toast that says "Published." The vocabulary of an interface is the signposting for someone navigating the product. Cohesion and consistency are how people learn their way around.
|
||||
|
||||
Treat failure and emptiness as moments for direction, not mood. Explain what went wrong and how to fix it, in the interface's voice rather than a person's. Errors don't apologize, and they are never vague about what happened. An empty screen is an invitation to act.
|
||||
|
||||
Keep the register conversational and tuned: plain verbs, sentence case, no filler, with tone matched to the brand and the audience. Let each element do exactly one job. A label labels, an example demonstrates, and nothing quietly does double duty.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "hookify",
|
||||
"version": "0.1.0",
|
||||
"description": "Easily create hooks to prevent unwanted behaviors by analyzing conversation patterns",
|
||||
"author": {
|
||||
"name": "Daisy Hollman",
|
||||
"email": "daisy@anthropic.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Local configuration (should not be committed)
|
||||
.claude/*.local.md
|
||||
.claude/*.local.json
|
||||
@@ -0,0 +1,340 @@
|
||||
# Hookify Plugin
|
||||
|
||||
Easily create custom hooks to prevent unwanted behaviors by analyzing conversation patterns or from explicit instructions.
|
||||
|
||||
## Overview
|
||||
|
||||
The hookify plugin makes it simple to create hooks without editing complex `hooks.json` files. Instead, you create lightweight markdown configuration files that define patterns to watch for and messages to show when those patterns match.
|
||||
|
||||
**Key features:**
|
||||
- 🎯 Analyze conversations to find unwanted behaviors automatically
|
||||
- 📝 Simple markdown configuration files with YAML frontmatter
|
||||
- 🔍 Regex pattern matching for powerful rules
|
||||
- 🚀 No coding required - just describe the behavior
|
||||
- 🔄 Easy enable/disable without restarting
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create Your First Rule
|
||||
|
||||
```bash
|
||||
/hookify Warn me when I use rm -rf commands
|
||||
```
|
||||
|
||||
This analyzes your request and creates `.claude/hookify.warn-rm.local.md`.
|
||||
|
||||
### 2. Test It Immediately
|
||||
|
||||
**No restart needed!** Rules take effect on the very next tool use.
|
||||
|
||||
Ask Claude to run a command that should trigger the rule:
|
||||
```
|
||||
Run rm -rf /tmp/test
|
||||
```
|
||||
|
||||
You should see the warning message immediately!
|
||||
|
||||
## Usage
|
||||
|
||||
### Main Command: /hookify
|
||||
|
||||
**With arguments:**
|
||||
```
|
||||
/hookify Don't use console.log in TypeScript files
|
||||
```
|
||||
Creates a rule from your explicit instructions.
|
||||
|
||||
**Without arguments:**
|
||||
```
|
||||
/hookify
|
||||
```
|
||||
Analyzes recent conversation to find behaviors you've corrected or been frustrated by.
|
||||
|
||||
### Helper Commands
|
||||
|
||||
**List all rules:**
|
||||
```
|
||||
/hookify:list
|
||||
```
|
||||
|
||||
**Configure rules interactively:**
|
||||
```
|
||||
/hookify:configure
|
||||
```
|
||||
Enable/disable existing rules through an interactive interface.
|
||||
|
||||
**Get help:**
|
||||
```
|
||||
/hookify:help
|
||||
```
|
||||
|
||||
## Rule Configuration Format
|
||||
|
||||
### Simple Rule (Single Pattern)
|
||||
|
||||
`.claude/hookify.dangerous-rm.local.md`:
|
||||
```markdown
|
||||
---
|
||||
name: block-dangerous-rm
|
||||
enabled: true
|
||||
event: bash
|
||||
pattern: rm\s+-rf
|
||||
action: block
|
||||
---
|
||||
|
||||
⚠️ **Dangerous rm command detected!**
|
||||
|
||||
This command could delete important files. Please:
|
||||
- Verify the path is correct
|
||||
- Consider using a safer approach
|
||||
- Make sure you have backups
|
||||
```
|
||||
|
||||
**Action field:**
|
||||
- `warn`: Shows warning but allows operation (default)
|
||||
- `block`: Prevents operation from executing (PreToolUse) or stops session (Stop events)
|
||||
|
||||
### Advanced Rule (Multiple Conditions)
|
||||
|
||||
`.claude/hookify.sensitive-files.local.md`:
|
||||
```markdown
|
||||
---
|
||||
name: warn-sensitive-files
|
||||
enabled: true
|
||||
event: file
|
||||
action: warn
|
||||
conditions:
|
||||
- field: file_path
|
||||
operator: regex_match
|
||||
pattern: \.env$|credentials|secrets
|
||||
- field: new_text
|
||||
operator: contains
|
||||
pattern: KEY
|
||||
---
|
||||
|
||||
🔐 **Sensitive file edit detected!**
|
||||
|
||||
Ensure credentials are not hardcoded and file is in .gitignore.
|
||||
```
|
||||
|
||||
**All conditions must match** for the rule to trigger.
|
||||
|
||||
## Event Types
|
||||
|
||||
- **`bash`**: Triggers on Bash tool commands
|
||||
- **`file`**: Triggers on Edit, Write, MultiEdit tools
|
||||
- **`stop`**: Triggers when Claude wants to stop (for completion checks)
|
||||
- **`prompt`**: Triggers on user prompt submission
|
||||
- **`all`**: Triggers on all events
|
||||
|
||||
## Pattern Syntax
|
||||
|
||||
Use Python regex syntax:
|
||||
|
||||
| Pattern | Matches | Example |
|
||||
|---------|---------|---------|
|
||||
| `rm\s+-rf` | rm -rf | rm -rf /tmp |
|
||||
| `console\.log\(` | console.log( | console.log("test") |
|
||||
| `(eval\|exec)\(` | eval( or exec( | eval("code") |
|
||||
| `\.env$` | files ending in .env | .env, .env.local |
|
||||
| `chmod\s+777` | chmod 777 | chmod 777 file.txt |
|
||||
|
||||
**Tips:**
|
||||
- Use `\s` for whitespace
|
||||
- Escape special chars: `\.` for literal dot
|
||||
- Use `|` for OR: `(foo|bar)`
|
||||
- Use `.*` to match anything
|
||||
- Set `action: block` for dangerous operations
|
||||
- Set `action: warn` (or omit) for informational warnings
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Block Dangerous Commands
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: block-destructive-ops
|
||||
enabled: true
|
||||
event: bash
|
||||
pattern: rm\s+-rf|dd\s+if=|mkfs|format
|
||||
action: block
|
||||
---
|
||||
|
||||
🛑 **Destructive operation detected!**
|
||||
|
||||
This command can cause data loss. Operation blocked for safety.
|
||||
Please verify the exact path and use a safer approach.
|
||||
```
|
||||
|
||||
**This rule blocks the operation** - Claude will not be allowed to execute these commands.
|
||||
|
||||
### Example 2: Warn About Debug Code
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: warn-debug-code
|
||||
enabled: true
|
||||
event: file
|
||||
pattern: console\.log\(|debugger;|print\(
|
||||
action: warn
|
||||
---
|
||||
|
||||
🐛 **Debug code detected**
|
||||
|
||||
Remember to remove debugging statements before committing.
|
||||
```
|
||||
|
||||
**This rule warns but allows** - Claude sees the message but can still proceed.
|
||||
|
||||
### Example 3: Require Tests Before Stopping
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: require-tests-run
|
||||
enabled: false
|
||||
event: stop
|
||||
action: block
|
||||
conditions:
|
||||
- field: transcript
|
||||
operator: not_contains
|
||||
pattern: npm test|pytest|cargo test
|
||||
---
|
||||
|
||||
**Tests not detected in transcript!**
|
||||
|
||||
Before stopping, please run tests to verify your changes work correctly.
|
||||
```
|
||||
|
||||
**This blocks Claude from stopping** if no test commands appear in the session transcript. Enable only when you want strict enforcement.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Multiple Conditions
|
||||
|
||||
Check multiple fields simultaneously:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: api-key-in-typescript
|
||||
enabled: true
|
||||
event: file
|
||||
conditions:
|
||||
- field: file_path
|
||||
operator: regex_match
|
||||
pattern: \.tsx?$
|
||||
- field: new_text
|
||||
operator: regex_match
|
||||
pattern: (API_KEY|SECRET|TOKEN)\s*=\s*["']
|
||||
---
|
||||
|
||||
🔐 **Hardcoded credential in TypeScript!**
|
||||
|
||||
Use environment variables instead of hardcoded values.
|
||||
```
|
||||
|
||||
### Operators Reference
|
||||
|
||||
- `regex_match`: Pattern must match (most common)
|
||||
- `contains`: String must contain pattern
|
||||
- `equals`: Exact string match
|
||||
- `not_contains`: String must NOT contain pattern
|
||||
- `starts_with`: String starts with pattern
|
||||
- `ends_with`: String ends with pattern
|
||||
|
||||
### Field Reference
|
||||
|
||||
**For bash events:**
|
||||
- `command`: The bash command string
|
||||
|
||||
**For file events:**
|
||||
- `file_path`: Path to file being edited
|
||||
- `new_text`: New content being added (Edit, Write)
|
||||
- `old_text`: Old content being replaced (Edit only)
|
||||
- `content`: File content (Write only)
|
||||
|
||||
**For prompt events:**
|
||||
- `user_prompt`: The user's submitted prompt text
|
||||
|
||||
**For stop events:**
|
||||
- Use general matching on session state
|
||||
|
||||
## Management
|
||||
|
||||
### Enable/Disable Rules
|
||||
|
||||
**Temporarily disable:**
|
||||
Edit the `.local.md` file and set `enabled: false`
|
||||
|
||||
**Re-enable:**
|
||||
Set `enabled: true`
|
||||
|
||||
**Or use interactive tool:**
|
||||
```
|
||||
/hookify:configure
|
||||
```
|
||||
|
||||
### Delete Rules
|
||||
|
||||
Simply delete the `.local.md` file:
|
||||
```bash
|
||||
rm .claude/hookify.my-rule.local.md
|
||||
```
|
||||
|
||||
### View All Rules
|
||||
|
||||
```
|
||||
/hookify:list
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
This plugin is part of the Claude Code Marketplace. It should be auto-discovered when the marketplace is installed.
|
||||
|
||||
**Manual testing:**
|
||||
```bash
|
||||
cc --plugin-dir /path/to/hookify
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- No external dependencies (uses stdlib only)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Rule not triggering:**
|
||||
1. Check rule file exists in `.claude/` directory (in project root, not plugin directory)
|
||||
2. Verify `enabled: true` in frontmatter
|
||||
3. Test regex pattern separately
|
||||
4. Rules should work immediately - no restart needed
|
||||
5. Try `/hookify:list` to see if rule is loaded
|
||||
|
||||
**Import errors:**
|
||||
- Ensure Python 3 is available: `python3 --version`
|
||||
- Check hookify plugin is installed
|
||||
|
||||
**Pattern not matching:**
|
||||
- Test regex: `python3 -c "import re; print(re.search(r'pattern', 'text'))"`
|
||||
- Use unquoted patterns in YAML to avoid escaping issues
|
||||
- Start simple, then add complexity
|
||||
|
||||
**Hook seems slow:**
|
||||
- Keep patterns simple (avoid complex regex)
|
||||
- Use specific event types (bash, file) instead of "all"
|
||||
- Limit number of active rules
|
||||
|
||||
## Contributing
|
||||
|
||||
Found a useful rule pattern? Consider sharing example files via PR!
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Severity levels (error/warning/info distinctions)
|
||||
- Rule templates library
|
||||
- Interactive pattern builder
|
||||
- Hook testing utilities
|
||||
- JSON format support (in addition to markdown)
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
name: conversation-analyzer
|
||||
description: Use this agent when analyzing conversation transcripts to find behaviors worth preventing with hooks. Examples: <example>Context: User is running /hookify command without arguments\nuser: "/hookify"\nassistant: "I'll analyze the conversation to find behaviors you want to prevent"\n<commentary>The /hookify command without arguments triggers conversation analysis to find unwanted behaviors.</commentary></example><example>Context: User wants to create hooks from recent frustrations\nuser: "Can you look back at this conversation and help me create hooks for the mistakes you made?"\nassistant: "I'll use the conversation-analyzer agent to identify the issues and suggest hooks."\n<commentary>User explicitly asks to analyze conversation for mistakes that should be prevented.</commentary></example>
|
||||
model: inherit
|
||||
color: yellow
|
||||
tools: ["Read", "Grep"]
|
||||
---
|
||||
|
||||
You are a conversation analysis specialist that identifies problematic behaviors in Claude Code sessions that could be prevented with hooks.
|
||||
|
||||
**Your Core Responsibilities:**
|
||||
1. Read and analyze user messages to find frustration signals
|
||||
2. Identify specific tool usage patterns that caused issues
|
||||
3. Extract actionable patterns that can be matched with regex
|
||||
4. Categorize issues by severity and type
|
||||
5. Provide structured findings for hook rule generation
|
||||
|
||||
**Analysis Process:**
|
||||
|
||||
### 1. Search for User Messages Indicating Issues
|
||||
|
||||
Read through user messages in reverse chronological order (most recent first). Look for:
|
||||
|
||||
**Explicit correction requests:**
|
||||
- "Don't use X"
|
||||
- "Stop doing Y"
|
||||
- "Please don't Z"
|
||||
- "Avoid..."
|
||||
- "Never..."
|
||||
|
||||
**Frustrated reactions:**
|
||||
- "Why did you do X?"
|
||||
- "I didn't ask for that"
|
||||
- "That's not what I meant"
|
||||
- "That was wrong"
|
||||
|
||||
**Corrections and reversions:**
|
||||
- User reverting changes Claude made
|
||||
- User fixing issues Claude created
|
||||
- User providing step-by-step corrections
|
||||
|
||||
**Repeated issues:**
|
||||
- Same type of mistake multiple times
|
||||
- User having to remind multiple times
|
||||
- Pattern of similar problems
|
||||
|
||||
### 2. Identify Tool Usage Patterns
|
||||
|
||||
For each issue, determine:
|
||||
- **Which tool**: Bash, Edit, Write, MultiEdit
|
||||
- **What action**: Specific command or code pattern
|
||||
- **When it happened**: During what task/phase
|
||||
- **Why problematic**: User's stated reason or implicit concern
|
||||
|
||||
**Extract concrete examples:**
|
||||
- For Bash: Actual command that was problematic
|
||||
- For Edit/Write: Code pattern that was added
|
||||
- For Stop: What was missing before stopping
|
||||
|
||||
### 3. Create Regex Patterns
|
||||
|
||||
Convert behaviors into matchable patterns:
|
||||
|
||||
**Bash command patterns:**
|
||||
- `rm\s+-rf` for dangerous deletes
|
||||
- `sudo\s+` for privilege escalation
|
||||
- `chmod\s+777` for permission issues
|
||||
|
||||
**Code patterns (Edit/Write):**
|
||||
- `console\.log\(` for debug logging
|
||||
- `eval\(|new Function\(` for dangerous eval
|
||||
- `innerHTML\s*=` for XSS risks
|
||||
|
||||
**File path patterns:**
|
||||
- `\.env$` for environment files
|
||||
- `/node_modules/` for dependency files
|
||||
- `dist/|build/` for generated files
|
||||
|
||||
### 4. Categorize Severity
|
||||
|
||||
**High severity (should block in future):**
|
||||
- Dangerous commands (rm -rf, chmod 777)
|
||||
- Security issues (hardcoded secrets, eval)
|
||||
- Data loss risks
|
||||
|
||||
**Medium severity (warn):**
|
||||
- Style violations (console.log in production)
|
||||
- Wrong file types (editing generated files)
|
||||
- Missing best practices
|
||||
|
||||
**Low severity (optional):**
|
||||
- Preferences (coding style)
|
||||
- Non-critical patterns
|
||||
|
||||
### 5. Output Format
|
||||
|
||||
Return your findings as structured text in this format:
|
||||
|
||||
```
|
||||
## Hookify Analysis Results
|
||||
|
||||
### Issue 1: Dangerous rm Commands
|
||||
**Severity**: High
|
||||
**Tool**: Bash
|
||||
**Pattern**: `rm\s+-rf`
|
||||
**Occurrences**: 3 times
|
||||
**Context**: Used rm -rf on /tmp directories without verification
|
||||
**User Reaction**: "Please be more careful with rm commands"
|
||||
|
||||
**Suggested Rule:**
|
||||
- Name: warn-dangerous-rm
|
||||
- Event: bash
|
||||
- Pattern: rm\s+-rf
|
||||
- Message: "Dangerous rm command detected. Verify path before proceeding."
|
||||
|
||||
---
|
||||
|
||||
### Issue 2: Console.log in TypeScript
|
||||
**Severity**: Medium
|
||||
**Tool**: Edit/Write
|
||||
**Pattern**: `console\.log\(`
|
||||
**Occurrences**: 2 times
|
||||
**Context**: Added console.log statements to production TypeScript files
|
||||
**User Reaction**: "Don't use console.log in production code"
|
||||
|
||||
**Suggested Rule:**
|
||||
- Name: warn-console-log
|
||||
- Event: file
|
||||
- Pattern: console\.log\(
|
||||
- Message: "Console.log detected. Use proper logging library instead."
|
||||
|
||||
---
|
||||
|
||||
[Continue for each issue found...]
|
||||
|
||||
## Summary
|
||||
|
||||
Found {N} behaviors worth preventing:
|
||||
- {N} high severity
|
||||
- {N} medium severity
|
||||
- {N} low severity
|
||||
|
||||
Recommend creating rules for high and medium severity issues.
|
||||
```
|
||||
|
||||
**Quality Standards:**
|
||||
- Be specific about patterns (don't be overly broad)
|
||||
- Include actual examples from conversation
|
||||
- Explain why each issue matters
|
||||
- Provide ready-to-use regex patterns
|
||||
- Don't false-positive on discussions about what NOT to do
|
||||
|
||||
**Edge Cases:**
|
||||
|
||||
**User discussing hypotheticals:**
|
||||
- "What would happen if I used rm -rf?"
|
||||
- Don't treat as problematic behavior
|
||||
|
||||
**Teaching moments:**
|
||||
- "Here's what you shouldn't do: ..."
|
||||
- Context indicates explanation, not actual problem
|
||||
|
||||
**One-time accidents:**
|
||||
- Single occurrence, already fixed
|
||||
- Mention but mark as low priority
|
||||
|
||||
**Subjective preferences:**
|
||||
- "I prefer X over Y"
|
||||
- Mark as low severity, let user decide
|
||||
|
||||
**Return Results:**
|
||||
Provide your analysis in the structured format above. The /hookify command will use this to:
|
||||
1. Present findings to user
|
||||
2. Ask which rules to create
|
||||
3. Generate .local.md configuration files
|
||||
4. Save rules to .claude directory
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
description: Enable or disable hookify rules interactively
|
||||
allowed-tools: ["Glob", "Read", "Edit", "AskUserQuestion", "Skill"]
|
||||
---
|
||||
|
||||
# Configure Hookify Rules
|
||||
|
||||
**Load hookify:writing-rules skill first** to understand rule format.
|
||||
|
||||
Enable or disable existing hookify rules using an interactive interface.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Find Existing Rules
|
||||
|
||||
Use Glob tool to find all hookify rule files:
|
||||
```
|
||||
pattern: ".claude/hookify.*.local.md"
|
||||
```
|
||||
|
||||
If no rules found, inform user:
|
||||
```
|
||||
No hookify rules configured yet. Use `/hookify` to create your first rule.
|
||||
```
|
||||
|
||||
### 2. Read Current State
|
||||
|
||||
For each rule file:
|
||||
- Read the file
|
||||
- Extract `name` and `enabled` fields from frontmatter
|
||||
- Build list of rules with current state
|
||||
|
||||
### 3. Ask User Which Rules to Toggle
|
||||
|
||||
Use AskUserQuestion to let user select rules:
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Which rules would you like to enable or disable?",
|
||||
"header": "Configure",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{
|
||||
"label": "warn-dangerous-rm (currently enabled)",
|
||||
"description": "Warns about rm -rf commands"
|
||||
},
|
||||
{
|
||||
"label": "warn-console-log (currently disabled)",
|
||||
"description": "Warns about console.log in code"
|
||||
},
|
||||
{
|
||||
"label": "require-tests (currently enabled)",
|
||||
"description": "Requires tests before stopping"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Option format:**
|
||||
- Label: `{rule-name} (currently {enabled|disabled})`
|
||||
- Description: Brief description from rule's message or pattern
|
||||
|
||||
### 4. Parse User Selection
|
||||
|
||||
For each selected rule:
|
||||
- Determine current state from label (enabled/disabled)
|
||||
- Toggle state: enabled → disabled, disabled → enabled
|
||||
|
||||
### 5. Update Rule Files
|
||||
|
||||
For each rule to toggle:
|
||||
- Use Read tool to read current content
|
||||
- Use Edit tool to change `enabled: true` to `enabled: false` (or vice versa)
|
||||
- Handle both with and without quotes
|
||||
|
||||
**Edit pattern for enabling:**
|
||||
```
|
||||
old_string: "enabled: false"
|
||||
new_string: "enabled: true"
|
||||
```
|
||||
|
||||
**Edit pattern for disabling:**
|
||||
```
|
||||
old_string: "enabled: true"
|
||||
new_string: "enabled: false"
|
||||
```
|
||||
|
||||
### 6. Confirm Changes
|
||||
|
||||
Show user what was changed:
|
||||
|
||||
```
|
||||
## Hookify Rules Updated
|
||||
|
||||
**Enabled:**
|
||||
- warn-console-log
|
||||
|
||||
**Disabled:**
|
||||
- warn-dangerous-rm
|
||||
|
||||
**Unchanged:**
|
||||
- require-tests
|
||||
|
||||
Changes apply immediately - no restart needed
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Changes take effect immediately on next tool use
|
||||
- You can also manually edit .claude/hookify.*.local.md files
|
||||
- To permanently remove a rule, delete its .local.md file
|
||||
- Use `/hookify:list` to see all configured rules
|
||||
|
||||
## Edge Cases
|
||||
|
||||
**No rules to configure:**
|
||||
- Show message about using `/hookify` to create rules first
|
||||
|
||||
**User selects no rules:**
|
||||
- Inform that no changes were made
|
||||
|
||||
**File read/write errors:**
|
||||
- Inform user of specific error
|
||||
- Suggest manual editing as fallback
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
description: Get help with the hookify plugin
|
||||
allowed-tools: ["Read"]
|
||||
---
|
||||
|
||||
# Hookify Plugin Help
|
||||
|
||||
Explain how the hookify plugin works and how to use it.
|
||||
|
||||
## Overview
|
||||
|
||||
The hookify plugin makes it easy to create custom hooks that prevent unwanted behaviors. Instead of editing `hooks.json` files, users create simple markdown configuration files that define patterns to watch for.
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Hook System
|
||||
|
||||
Hookify installs generic hooks that run on these events:
|
||||
- **PreToolUse**: Before any tool executes (Bash, Edit, Write, etc.)
|
||||
- **PostToolUse**: After a tool executes
|
||||
- **Stop**: When Claude wants to stop working
|
||||
- **UserPromptSubmit**: When user submits a prompt
|
||||
|
||||
These hooks read configuration files from `.claude/hookify.*.local.md` and check if any rules match the current operation.
|
||||
|
||||
### 2. Configuration Files
|
||||
|
||||
Users create rules in `.claude/hookify.{rule-name}.local.md` files:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: warn-dangerous-rm
|
||||
enabled: true
|
||||
event: bash
|
||||
pattern: rm\s+-rf
|
||||
---
|
||||
|
||||
⚠️ **Dangerous rm command detected!**
|
||||
|
||||
This command could delete important files. Please verify the path.
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
- `name`: Unique identifier for the rule
|
||||
- `enabled`: true/false to activate/deactivate
|
||||
- `event`: bash, file, stop, prompt, or all
|
||||
- `pattern`: Regex pattern to match
|
||||
|
||||
The message body is what Claude sees when the rule triggers.
|
||||
|
||||
### 3. Creating Rules
|
||||
|
||||
**Option A: Use /hookify command**
|
||||
```
|
||||
/hookify Don't use console.log in production files
|
||||
```
|
||||
|
||||
This analyzes your request and creates the appropriate rule file.
|
||||
|
||||
**Option B: Create manually**
|
||||
Create `.claude/hookify.my-rule.local.md` with the format above.
|
||||
|
||||
**Option C: Analyze conversation**
|
||||
```
|
||||
/hookify
|
||||
```
|
||||
|
||||
Without arguments, hookify analyzes recent conversation to find behaviors you want to prevent.
|
||||
|
||||
## Available Commands
|
||||
|
||||
- **`/hookify`** - Create hooks from conversation analysis or explicit instructions
|
||||
- **`/hookify:help`** - Show this help (what you're reading now)
|
||||
- **`/hookify:list`** - List all configured hooks
|
||||
- **`/hookify:configure`** - Enable/disable existing hooks interactively
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
**Prevent dangerous commands:**
|
||||
```markdown
|
||||
---
|
||||
name: block-chmod-777
|
||||
enabled: true
|
||||
event: bash
|
||||
pattern: chmod\s+777
|
||||
---
|
||||
|
||||
Don't use chmod 777 - it's a security risk. Use specific permissions instead.
|
||||
```
|
||||
|
||||
**Warn about debugging code:**
|
||||
```markdown
|
||||
---
|
||||
name: warn-console-log
|
||||
enabled: true
|
||||
event: file
|
||||
pattern: console\.log\(
|
||||
---
|
||||
|
||||
Console.log detected. Remember to remove debug logging before committing.
|
||||
```
|
||||
|
||||
**Require tests before stopping:**
|
||||
```markdown
|
||||
---
|
||||
name: require-tests
|
||||
enabled: true
|
||||
event: stop
|
||||
pattern: .*
|
||||
---
|
||||
|
||||
Did you run tests before finishing? Make sure `npm test` or equivalent was executed.
|
||||
```
|
||||
|
||||
## Pattern Syntax
|
||||
|
||||
Use Python regex syntax:
|
||||
- `\s` - whitespace
|
||||
- `\.` - literal dot
|
||||
- `|` - OR
|
||||
- `+` - one or more
|
||||
- `*` - zero or more
|
||||
- `\d` - digit
|
||||
- `[abc]` - character class
|
||||
|
||||
**Examples:**
|
||||
- `rm\s+-rf` - matches "rm -rf"
|
||||
- `console\.log\(` - matches "console.log("
|
||||
- `(eval|exec)\(` - matches "eval(" or "exec("
|
||||
- `\.env$` - matches files ending in .env
|
||||
|
||||
## Important Notes
|
||||
|
||||
**No Restart Needed**: Hookify rules (`.local.md` files) take effect immediately on the next tool use. The hookify hooks are already loaded and read your rules dynamically.
|
||||
|
||||
**Block or Warn**: Rules can either `block` operations (prevent execution) or `warn` (show message but allow). Set `action: block` or `action: warn` in the rule's frontmatter.
|
||||
|
||||
**Rule Files**: Keep rules in `.claude/hookify.*.local.md` - they should be git-ignored (add to .gitignore if needed).
|
||||
|
||||
**Disable Rules**: Set `enabled: false` in frontmatter or delete the file.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Hook not triggering:**
|
||||
- Check rule file is in `.claude/` directory
|
||||
- Verify `enabled: true` in frontmatter
|
||||
- Confirm pattern is valid regex
|
||||
- Test pattern: `python3 -c "import re; print(re.search('your_pattern', 'test_text'))"`
|
||||
- Rules take effect immediately - no restart needed
|
||||
|
||||
**Import errors:**
|
||||
- Check Python 3 is available: `python3 --version`
|
||||
- Verify hookify plugin is installed correctly
|
||||
|
||||
**Pattern not matching:**
|
||||
- Test regex separately
|
||||
- Check for escaping issues (use unquoted patterns in YAML)
|
||||
- Try simpler pattern first, then refine
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Create your first rule:
|
||||
```
|
||||
/hookify Warn me when I try to use rm -rf
|
||||
```
|
||||
|
||||
2. Try to trigger it:
|
||||
- Ask Claude to run `rm -rf /tmp/test`
|
||||
- You should see the warning
|
||||
|
||||
4. Refine the rule by editing `.claude/hookify.warn-rm.local.md`
|
||||
|
||||
5. Create more rules as you encounter unwanted behaviors
|
||||
|
||||
For more examples, check the `${CLAUDE_PLUGIN_ROOT}/examples/` directory.
|
||||
@@ -0,0 +1,231 @@
|
||||
---
|
||||
description: Create hooks to prevent unwanted behaviors from conversation analysis or explicit instructions
|
||||
argument-hint: Optional specific behavior to address
|
||||
allowed-tools: ["Read", "Write", "AskUserQuestion", "Task", "Grep", "TodoWrite", "Skill"]
|
||||
---
|
||||
|
||||
# Hookify - Create Hooks from Unwanted Behaviors
|
||||
|
||||
**FIRST: Load the hookify:writing-rules skill** using the Skill tool to understand rule file format and syntax.
|
||||
|
||||
Create hook rules to prevent problematic behaviors by analyzing the conversation or from explicit user instructions.
|
||||
|
||||
## Your Task
|
||||
|
||||
You will help the user create hookify rules to prevent unwanted behaviors. Follow these steps:
|
||||
|
||||
### Step 1: Gather Behavior Information
|
||||
|
||||
**If $ARGUMENTS is provided:**
|
||||
- User has given specific instructions: `$ARGUMENTS`
|
||||
- Still analyze recent conversation (last 10-15 user messages) for additional context
|
||||
- Look for examples of the behavior happening
|
||||
|
||||
**If $ARGUMENTS is empty:**
|
||||
- Launch the conversation-analyzer agent to find problematic behaviors
|
||||
- Agent will scan user prompts for frustration signals
|
||||
- Agent will return structured findings
|
||||
|
||||
**To analyze conversation:**
|
||||
Use the Task tool to launch conversation-analyzer agent:
|
||||
```
|
||||
{
|
||||
"subagent_type": "general-purpose",
|
||||
"description": "Analyze conversation for unwanted behaviors",
|
||||
"prompt": "You are analyzing a Claude Code conversation to find behaviors the user wants to prevent.
|
||||
|
||||
Read user messages in the current conversation and identify:
|
||||
1. Explicit requests to avoid something (\"don't do X\", \"stop doing Y\")
|
||||
2. Corrections or reversions (user fixing Claude's actions)
|
||||
3. Frustrated reactions (\"why did you do X?\", \"I didn't ask for that\")
|
||||
4. Repeated issues (same problem multiple times)
|
||||
|
||||
For each issue found, extract:
|
||||
- What tool was used (Bash, Edit, Write, etc.)
|
||||
- Specific pattern or command
|
||||
- Why it was problematic
|
||||
- User's stated reason
|
||||
|
||||
Return findings as a structured list with:
|
||||
- category: Type of issue
|
||||
- tool: Which tool was involved
|
||||
- pattern: Regex or literal pattern to match
|
||||
- context: What happened
|
||||
- severity: high/medium/low
|
||||
|
||||
Focus on the most recent issues (last 20-30 messages). Don't go back further unless explicitly asked."
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Present Findings to User
|
||||
|
||||
After gathering behaviors (from arguments or agent), present to user using AskUserQuestion:
|
||||
|
||||
**Question 1: Which behaviors to hookify?**
|
||||
- Header: "Create Rules"
|
||||
- multiSelect: true
|
||||
- Options: List each detected behavior (max 4)
|
||||
- Label: Short description (e.g., "Block rm -rf")
|
||||
- Description: Why it's problematic
|
||||
|
||||
**Question 2: For each selected behavior, ask about action:**
|
||||
- "Should this block the operation or just warn?"
|
||||
- Options:
|
||||
- "Just warn" (action: warn - shows message but allows)
|
||||
- "Block operation" (action: block - prevents execution)
|
||||
|
||||
**Question 3: Ask for example patterns:**
|
||||
- "What patterns should trigger this rule?"
|
||||
- Show detected patterns
|
||||
- Allow user to refine or add more
|
||||
|
||||
### Step 3: Generate Rule Files
|
||||
|
||||
For each confirmed behavior, create a `.claude/hookify.{rule-name}.local.md` file:
|
||||
|
||||
**Rule naming convention:**
|
||||
- Use kebab-case
|
||||
- Be descriptive: `block-dangerous-rm`, `warn-console-log`, `require-tests-before-stop`
|
||||
- Start with action verb: block, warn, prevent, require
|
||||
|
||||
**File format:**
|
||||
```markdown
|
||||
---
|
||||
name: {rule-name}
|
||||
enabled: true
|
||||
event: {bash|file|stop|prompt|all}
|
||||
pattern: {regex pattern}
|
||||
action: {warn|block}
|
||||
---
|
||||
|
||||
{Message to show Claude when rule triggers}
|
||||
```
|
||||
|
||||
**Action values:**
|
||||
- `warn`: Show message but allow operation (default)
|
||||
- `block`: Prevent operation or stop session
|
||||
|
||||
**For more complex rules (multiple conditions):**
|
||||
```markdown
|
||||
---
|
||||
name: {rule-name}
|
||||
enabled: true
|
||||
event: file
|
||||
conditions:
|
||||
- field: file_path
|
||||
operator: regex_match
|
||||
pattern: \.env$
|
||||
- field: new_text
|
||||
operator: contains
|
||||
pattern: API_KEY
|
||||
---
|
||||
|
||||
{Warning message}
|
||||
```
|
||||
|
||||
### Step 4: Create Files and Confirm
|
||||
|
||||
**IMPORTANT**: Rule files must be created in the current working directory's `.claude/` folder, NOT the plugin directory.
|
||||
|
||||
Use the current working directory (where Claude Code was started) as the base path.
|
||||
|
||||
1. Check if `.claude/` directory exists in current working directory
|
||||
- If not, create it first with: `mkdir -p .claude`
|
||||
|
||||
2. Use Write tool to create each `.claude/hookify.{name}.local.md` file
|
||||
- Use relative path from current working directory: `.claude/hookify.{name}.local.md`
|
||||
- The path should resolve to the project's .claude directory, not the plugin's
|
||||
|
||||
3. Show user what was created:
|
||||
```
|
||||
Created 3 hookify rules:
|
||||
- .claude/hookify.dangerous-rm.local.md
|
||||
- .claude/hookify.console-log.local.md
|
||||
- .claude/hookify.sensitive-files.local.md
|
||||
|
||||
These rules will trigger on:
|
||||
- dangerous-rm: Bash commands matching "rm -rf"
|
||||
- console-log: Edits adding console.log statements
|
||||
- sensitive-files: Edits to .env or credentials files
|
||||
```
|
||||
|
||||
4. Verify files were created in the correct location by listing them
|
||||
|
||||
5. Inform user: **"Rules are active immediately - no restart needed!"**
|
||||
|
||||
The hookify hooks are already loaded and will read your new rules on the next tool use.
|
||||
|
||||
## Event Types Reference
|
||||
|
||||
- **bash**: Matches Bash tool commands
|
||||
- **file**: Matches Edit, Write, MultiEdit tools
|
||||
- **stop**: Matches when agent wants to stop (use for completion checks)
|
||||
- **prompt**: Matches when user submits prompts
|
||||
- **all**: Matches all events
|
||||
|
||||
## Pattern Writing Tips
|
||||
|
||||
**Bash patterns:**
|
||||
- Match dangerous commands: `rm\s+-rf|chmod\s+777|dd\s+if=`
|
||||
- Match specific tools: `npm\s+install\s+|pip\s+install`
|
||||
|
||||
**File patterns:**
|
||||
- Match code patterns: `console\.log\(|eval\(|innerHTML\s*=`
|
||||
- Match file paths: `\.env$|\.git/|node_modules/`
|
||||
|
||||
**Stop patterns:**
|
||||
- Check for missing steps: (check transcript or completion criteria)
|
||||
|
||||
## Example Workflow
|
||||
|
||||
**User says**: "/hookify Don't use rm -rf without asking me first"
|
||||
|
||||
**Your response**:
|
||||
1. Analyze: User wants to prevent rm -rf commands
|
||||
2. Ask: "Should I block this command or just warn you?"
|
||||
3. User selects: "Just warn"
|
||||
4. Create `.claude/hookify.dangerous-rm.local.md`:
|
||||
```markdown
|
||||
---
|
||||
name: warn-dangerous-rm
|
||||
enabled: true
|
||||
event: bash
|
||||
pattern: rm\s+-rf
|
||||
---
|
||||
|
||||
⚠️ **Dangerous rm command detected**
|
||||
|
||||
You requested to be warned before using rm -rf.
|
||||
Please verify the path is correct.
|
||||
```
|
||||
5. Confirm: "Created hookify rule. It's active immediately - try triggering it!"
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **No restart needed**: Rules take effect immediately on the next tool use
|
||||
- **File location**: Create files in project's `.claude/` directory (current working directory), NOT the plugin's .claude/
|
||||
- **Regex syntax**: Use Python regex syntax (raw strings, no need to escape in YAML)
|
||||
- **Action types**: Rules can `warn` (default) or `block` operations
|
||||
- **Testing**: Test rules immediately after creating them
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**If rule file creation fails:**
|
||||
1. Check current working directory with pwd
|
||||
2. Ensure `.claude/` directory exists (create with mkdir if needed)
|
||||
3. Use absolute path if needed: `{cwd}/.claude/hookify.{name}.local.md`
|
||||
4. Verify file was created with Glob or ls
|
||||
|
||||
**If rule doesn't trigger after creation:**
|
||||
1. Verify file is in project `.claude/` not plugin `.claude/`
|
||||
2. Check file with Read tool to ensure pattern is correct
|
||||
3. Test pattern with: `python3 -c "import re; print(re.search(r'pattern', 'test text'))"`
|
||||
4. Verify `enabled: true` in frontmatter
|
||||
5. Remember: Rules work immediately, no restart needed
|
||||
|
||||
**If blocking seems too strict:**
|
||||
1. Change `action: block` to `action: warn` in the rule file
|
||||
2. Or adjust the pattern to be more specific
|
||||
3. Changes take effect on next tool use
|
||||
|
||||
Use TodoWrite to track your progress through the steps.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
description: List all configured hookify rules
|
||||
allowed-tools: ["Glob", "Read", "Skill"]
|
||||
---
|
||||
|
||||
# List Hookify Rules
|
||||
|
||||
**Load hookify:writing-rules skill first** to understand rule format.
|
||||
|
||||
Show all configured hookify rules in the project.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Use Glob tool to find all hookify rule files:
|
||||
```
|
||||
pattern: ".claude/hookify.*.local.md"
|
||||
```
|
||||
|
||||
2. For each file found:
|
||||
- Use Read tool to read the file
|
||||
- Extract frontmatter fields: name, enabled, event, pattern
|
||||
- Extract message preview (first 100 chars)
|
||||
|
||||
3. Present results in a table:
|
||||
|
||||
```
|
||||
## Configured Hookify Rules
|
||||
|
||||
| Name | Enabled | Event | Pattern | File |
|
||||
|------|---------|-------|---------|------|
|
||||
| warn-dangerous-rm | ✅ Yes | bash | rm\s+-rf | hookify.dangerous-rm.local.md |
|
||||
| warn-console-log | ✅ Yes | file | console\.log\( | hookify.console-log.local.md |
|
||||
| check-tests | ❌ No | stop | .* | hookify.require-tests.local.md |
|
||||
|
||||
**Total**: 3 rules (2 enabled, 1 disabled)
|
||||
```
|
||||
|
||||
4. For each rule, show a brief preview:
|
||||
```
|
||||
### warn-dangerous-rm
|
||||
**Event**: bash
|
||||
**Pattern**: `rm\s+-rf`
|
||||
**Message**: "⚠️ **Dangerous rm command detected!** This command could delete..."
|
||||
|
||||
**Status**: ✅ Active
|
||||
**File**: .claude/hookify.dangerous-rm.local.md
|
||||
```
|
||||
|
||||
5. Add helpful footer:
|
||||
```
|
||||
---
|
||||
|
||||
To modify a rule: Edit the .local.md file directly
|
||||
To disable a rule: Set `enabled: false` in frontmatter
|
||||
To enable a rule: Set `enabled: true` in frontmatter
|
||||
To delete a rule: Remove the .local.md file
|
||||
To create a rule: Use `/hookify` command
|
||||
|
||||
**Remember**: Changes take effect immediately - no restart needed
|
||||
```
|
||||
|
||||
## If No Rules Found
|
||||
|
||||
If no hookify rules exist:
|
||||
|
||||
```
|
||||
## No Hookify Rules Configured
|
||||
|
||||
You haven't created any hookify rules yet.
|
||||
|
||||
To get started:
|
||||
1. Use `/hookify` to analyze conversation and create rules
|
||||
2. Or manually create `.claude/hookify.my-rule.local.md` files
|
||||
3. See `/hookify:help` for documentation
|
||||
|
||||
Example:
|
||||
```
|
||||
/hookify Warn me when I use console.log
|
||||
```
|
||||
|
||||
Check `${CLAUDE_PLUGIN_ROOT}/examples/` for example rule files.
|
||||
```
|
||||
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Configuration loader for hookify plugin.
|
||||
|
||||
Loads and parses .claude/hookify.*.local.md files.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import re
|
||||
from typing import List, Optional, Dict, Any
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class Condition:
|
||||
"""A single condition for matching."""
|
||||
field: str # "command", "new_text", "old_text", "file_path", etc.
|
||||
operator: str # "regex_match", "contains", "equals", etc.
|
||||
pattern: str # Pattern to match
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Condition':
|
||||
"""Create Condition from dict."""
|
||||
return cls(
|
||||
field=data.get('field', ''),
|
||||
operator=data.get('operator', 'regex_match'),
|
||||
pattern=data.get('pattern', '')
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Rule:
|
||||
"""A hookify rule."""
|
||||
name: str
|
||||
enabled: bool
|
||||
event: str # "bash", "file", "stop", "all", etc.
|
||||
pattern: Optional[str] = None # Simple pattern (legacy)
|
||||
conditions: List[Condition] = field(default_factory=list)
|
||||
action: str = "warn" # "warn" or "block" (future)
|
||||
tool_matcher: Optional[str] = None # Override tool matching
|
||||
message: str = "" # Message body from markdown
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, frontmatter: Dict[str, Any], message: str) -> 'Rule':
|
||||
"""Create Rule from frontmatter dict and message body."""
|
||||
# Handle both simple pattern and complex conditions
|
||||
conditions = []
|
||||
|
||||
# New style: explicit conditions list
|
||||
if 'conditions' in frontmatter:
|
||||
cond_list = frontmatter['conditions']
|
||||
if isinstance(cond_list, list):
|
||||
conditions = [Condition.from_dict(c) for c in cond_list]
|
||||
|
||||
# Legacy style: simple pattern field
|
||||
simple_pattern = frontmatter.get('pattern')
|
||||
if simple_pattern and not conditions:
|
||||
# Convert simple pattern to condition
|
||||
# Infer field from event
|
||||
event = frontmatter.get('event', 'all')
|
||||
if event == 'bash':
|
||||
field = 'command'
|
||||
elif event == 'file':
|
||||
field = 'new_text'
|
||||
else:
|
||||
field = 'content'
|
||||
|
||||
conditions = [Condition(
|
||||
field=field,
|
||||
operator='regex_match',
|
||||
pattern=simple_pattern
|
||||
)]
|
||||
|
||||
return cls(
|
||||
name=frontmatter.get('name', 'unnamed'),
|
||||
enabled=frontmatter.get('enabled', True),
|
||||
event=frontmatter.get('event', 'all'),
|
||||
pattern=simple_pattern,
|
||||
conditions=conditions,
|
||||
action=frontmatter.get('action', 'warn'),
|
||||
tool_matcher=frontmatter.get('tool_matcher'),
|
||||
message=message.strip()
|
||||
)
|
||||
|
||||
|
||||
def extract_frontmatter(content: str) -> tuple[Dict[str, Any], str]:
|
||||
"""Extract YAML frontmatter and message body from markdown.
|
||||
|
||||
Returns (frontmatter_dict, message_body).
|
||||
|
||||
Supports multi-line dictionary items in lists by preserving indentation.
|
||||
"""
|
||||
if not content.startswith('---'):
|
||||
return {}, content
|
||||
|
||||
# Split on --- markers
|
||||
parts = content.split('---', 2)
|
||||
if len(parts) < 3:
|
||||
return {}, content
|
||||
|
||||
frontmatter_text = parts[1]
|
||||
message = parts[2].strip()
|
||||
|
||||
# Simple YAML parser that handles indented list items
|
||||
frontmatter = {}
|
||||
lines = frontmatter_text.split('\n')
|
||||
|
||||
current_key = None
|
||||
current_list = []
|
||||
current_dict = {}
|
||||
in_list = False
|
||||
in_dict_item = False
|
||||
|
||||
for line in lines:
|
||||
# Skip empty lines and comments
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith('#'):
|
||||
continue
|
||||
|
||||
# Check indentation level
|
||||
indent = len(line) - len(line.lstrip())
|
||||
|
||||
# Top-level key (no indentation or minimal)
|
||||
if indent == 0 and ':' in line and not line.strip().startswith('-'):
|
||||
# Save previous list/dict if any
|
||||
if in_list and current_key:
|
||||
if in_dict_item and current_dict:
|
||||
current_list.append(current_dict)
|
||||
current_dict = {}
|
||||
frontmatter[current_key] = current_list
|
||||
in_list = False
|
||||
in_dict_item = False
|
||||
current_list = []
|
||||
|
||||
key, value = line.split(':', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
if not value:
|
||||
# Empty value - list or nested structure follows
|
||||
current_key = key
|
||||
in_list = True
|
||||
current_list = []
|
||||
else:
|
||||
# Simple key-value pair
|
||||
value = value.strip('"').strip("'")
|
||||
if value.lower() == 'true':
|
||||
value = True
|
||||
elif value.lower() == 'false':
|
||||
value = False
|
||||
frontmatter[key] = value
|
||||
|
||||
# List item (starts with -)
|
||||
elif stripped.startswith('-') and in_list:
|
||||
# Save previous dict item if any
|
||||
if in_dict_item and current_dict:
|
||||
current_list.append(current_dict)
|
||||
current_dict = {}
|
||||
|
||||
item_text = stripped[1:].strip()
|
||||
|
||||
# Check if this is an inline dict (key: value on same line)
|
||||
if ':' in item_text and ',' in item_text:
|
||||
# Inline comma-separated dict: "- field: command, operator: regex_match"
|
||||
item_dict = {}
|
||||
for part in item_text.split(','):
|
||||
if ':' in part:
|
||||
k, v = part.split(':', 1)
|
||||
item_dict[k.strip()] = v.strip().strip('"').strip("'")
|
||||
current_list.append(item_dict)
|
||||
in_dict_item = False
|
||||
elif ':' in item_text:
|
||||
# Start of multi-line dict item: "- field: command"
|
||||
in_dict_item = True
|
||||
k, v = item_text.split(':', 1)
|
||||
current_dict = {k.strip(): v.strip().strip('"').strip("'")}
|
||||
else:
|
||||
# Simple list item
|
||||
current_list.append(item_text.strip('"').strip("'"))
|
||||
in_dict_item = False
|
||||
|
||||
# Continuation of dict item (indented under list item)
|
||||
elif indent > 2 and in_dict_item and ':' in line:
|
||||
# This is a field of the current dict item
|
||||
k, v = stripped.split(':', 1)
|
||||
current_dict[k.strip()] = v.strip().strip('"').strip("'")
|
||||
|
||||
# Save final list/dict if any
|
||||
if in_list and current_key:
|
||||
if in_dict_item and current_dict:
|
||||
current_list.append(current_dict)
|
||||
frontmatter[current_key] = current_list
|
||||
|
||||
return frontmatter, message
|
||||
|
||||
|
||||
def load_rules(event: Optional[str] = None) -> List[Rule]:
|
||||
"""Load all hookify rules from .claude directory.
|
||||
|
||||
Args:
|
||||
event: Optional event filter ("bash", "file", "stop", etc.)
|
||||
|
||||
Returns:
|
||||
List of enabled Rule objects matching the event.
|
||||
"""
|
||||
rules = []
|
||||
|
||||
# Find all hookify.*.local.md files
|
||||
pattern = os.path.join('.claude', 'hookify.*.local.md')
|
||||
files = glob.glob(pattern)
|
||||
|
||||
for file_path in files:
|
||||
try:
|
||||
rule = load_rule_file(file_path)
|
||||
if not rule:
|
||||
continue
|
||||
|
||||
# Filter by event if specified
|
||||
if event:
|
||||
if rule.event != 'all' and rule.event != event:
|
||||
continue
|
||||
|
||||
# Only include enabled rules
|
||||
if rule.enabled:
|
||||
rules.append(rule)
|
||||
|
||||
except (IOError, OSError, PermissionError) as e:
|
||||
# File I/O errors - log and continue
|
||||
print(f"Warning: Failed to read {file_path}: {e}", file=sys.stderr)
|
||||
continue
|
||||
except (ValueError, KeyError, AttributeError, TypeError) as e:
|
||||
# Parsing errors - log and continue
|
||||
print(f"Warning: Failed to parse {file_path}: {e}", file=sys.stderr)
|
||||
continue
|
||||
except Exception as e:
|
||||
# Unexpected errors - log with type details
|
||||
print(f"Warning: Unexpected error loading {file_path} ({type(e).__name__}): {e}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
return rules
|
||||
|
||||
|
||||
def load_rule_file(file_path: str) -> Optional[Rule]:
|
||||
"""Load a single rule file.
|
||||
|
||||
Returns:
|
||||
Rule object or None if file is invalid.
|
||||
"""
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
frontmatter, message = extract_frontmatter(content)
|
||||
|
||||
if not frontmatter:
|
||||
print(f"Warning: {file_path} missing YAML frontmatter (must start with ---)", file=sys.stderr)
|
||||
return None
|
||||
|
||||
rule = Rule.from_dict(frontmatter, message)
|
||||
return rule
|
||||
|
||||
except (IOError, OSError, PermissionError) as e:
|
||||
print(f"Error: Cannot read {file_path}: {e}", file=sys.stderr)
|
||||
return None
|
||||
except (ValueError, KeyError, AttributeError, TypeError) as e:
|
||||
print(f"Error: Malformed rule file {file_path}: {e}", file=sys.stderr)
|
||||
return None
|
||||
except UnicodeDecodeError as e:
|
||||
print(f"Error: Invalid encoding in {file_path}: {e}", file=sys.stderr)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error: Unexpected error parsing {file_path} ({type(e).__name__}): {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
# For testing
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
# Test frontmatter parsing
|
||||
test_content = """---
|
||||
name: test-rule
|
||||
enabled: true
|
||||
event: bash
|
||||
pattern: "rm -rf"
|
||||
---
|
||||
|
||||
⚠️ Dangerous command detected!
|
||||
"""
|
||||
|
||||
fm, msg = extract_frontmatter(test_content)
|
||||
print("Frontmatter:", fm)
|
||||
print("Message:", msg)
|
||||
|
||||
rule = Rule.from_dict(fm, msg)
|
||||
print("Rule:", rule)
|
||||
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rule evaluation engine for hookify plugin."""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
# Import from local module
|
||||
from hookify.core.config_loader import Rule, Condition
|
||||
|
||||
|
||||
# Cache compiled regexes (max 128 patterns)
|
||||
@lru_cache(maxsize=128)
|
||||
def compile_regex(pattern: str) -> re.Pattern:
|
||||
"""Compile regex pattern with caching.
|
||||
|
||||
Args:
|
||||
pattern: Regex pattern string
|
||||
|
||||
Returns:
|
||||
Compiled regex pattern
|
||||
"""
|
||||
return re.compile(pattern, re.IGNORECASE)
|
||||
|
||||
|
||||
class RuleEngine:
|
||||
"""Evaluates rules against hook input data."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize rule engine."""
|
||||
# No need for instance cache anymore - using global lru_cache
|
||||
pass
|
||||
|
||||
def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Evaluate all rules and return combined results.
|
||||
|
||||
Checks all rules and accumulates matches. Blocking rules take priority
|
||||
over warning rules. All matching rule messages are combined.
|
||||
|
||||
Args:
|
||||
rules: List of Rule objects to evaluate
|
||||
input_data: Hook input JSON (tool_name, tool_input, etc.)
|
||||
|
||||
Returns:
|
||||
Response dict with systemMessage, hookSpecificOutput, etc.
|
||||
Empty dict {} if no rules match.
|
||||
"""
|
||||
hook_event = input_data.get('hook_event_name', '')
|
||||
blocking_rules = []
|
||||
warning_rules = []
|
||||
|
||||
for rule in rules:
|
||||
if self._rule_matches(rule, input_data):
|
||||
if rule.action == 'block':
|
||||
blocking_rules.append(rule)
|
||||
else:
|
||||
warning_rules.append(rule)
|
||||
|
||||
# If any blocking rules matched, block the operation
|
||||
if blocking_rules:
|
||||
messages = [f"**[{r.name}]**\n{r.message}" for r in blocking_rules]
|
||||
combined_message = "\n\n".join(messages)
|
||||
|
||||
# Use appropriate blocking format based on event type
|
||||
if hook_event == 'Stop':
|
||||
return {
|
||||
"decision": "block",
|
||||
"reason": combined_message,
|
||||
"systemMessage": combined_message
|
||||
}
|
||||
elif hook_event in ['PreToolUse', 'PostToolUse']:
|
||||
return {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": hook_event,
|
||||
"permissionDecision": "deny"
|
||||
},
|
||||
"systemMessage": combined_message
|
||||
}
|
||||
else:
|
||||
# For other events, just show message
|
||||
return {
|
||||
"systemMessage": combined_message
|
||||
}
|
||||
|
||||
# If only warnings, show them but allow operation
|
||||
if warning_rules:
|
||||
messages = [f"**[{r.name}]**\n{r.message}" for r in warning_rules]
|
||||
return {
|
||||
"systemMessage": "\n\n".join(messages)
|
||||
}
|
||||
|
||||
# No matches - allow operation
|
||||
return {}
|
||||
|
||||
def _rule_matches(self, rule: Rule, input_data: Dict[str, Any]) -> bool:
|
||||
"""Check if rule matches input data.
|
||||
|
||||
Args:
|
||||
rule: Rule to evaluate
|
||||
input_data: Hook input data
|
||||
|
||||
Returns:
|
||||
True if rule matches, False otherwise
|
||||
"""
|
||||
# Extract tool information
|
||||
tool_name = input_data.get('tool_name', '')
|
||||
tool_input = input_data.get('tool_input', {})
|
||||
|
||||
# Check tool matcher if specified
|
||||
if rule.tool_matcher:
|
||||
if not self._matches_tool(rule.tool_matcher, tool_name):
|
||||
return False
|
||||
|
||||
# If no conditions, don't match
|
||||
# (Rules must have at least one condition to be valid)
|
||||
if not rule.conditions:
|
||||
return False
|
||||
|
||||
# All conditions must match
|
||||
for condition in rule.conditions:
|
||||
if not self._check_condition(condition, tool_name, tool_input, input_data):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _matches_tool(self, matcher: str, tool_name: str) -> bool:
|
||||
"""Check if tool_name matches the matcher pattern.
|
||||
|
||||
Args:
|
||||
matcher: Pattern like "Bash", "Edit|Write", "*"
|
||||
tool_name: Actual tool name
|
||||
|
||||
Returns:
|
||||
True if matches
|
||||
"""
|
||||
if matcher == '*':
|
||||
return True
|
||||
|
||||
# Split on | for OR matching
|
||||
patterns = matcher.split('|')
|
||||
return tool_name in patterns
|
||||
|
||||
def _check_condition(self, condition: Condition, tool_name: str,
|
||||
tool_input: Dict[str, Any], input_data: Dict[str, Any] = None) -> bool:
|
||||
"""Check if a single condition matches.
|
||||
|
||||
Args:
|
||||
condition: Condition to check
|
||||
tool_name: Tool being used
|
||||
tool_input: Tool input dict
|
||||
input_data: Full hook input data (for Stop events, etc.)
|
||||
|
||||
Returns:
|
||||
True if condition matches
|
||||
"""
|
||||
# Extract the field value to check
|
||||
field_value = self._extract_field(condition.field, tool_name, tool_input, input_data)
|
||||
if field_value is None:
|
||||
return False
|
||||
|
||||
# Apply operator
|
||||
operator = condition.operator
|
||||
pattern = condition.pattern
|
||||
|
||||
if operator == 'regex_match':
|
||||
return self._regex_match(pattern, field_value)
|
||||
elif operator == 'contains':
|
||||
return pattern in field_value
|
||||
elif operator == 'equals':
|
||||
return pattern == field_value
|
||||
elif operator == 'not_contains':
|
||||
return pattern not in field_value
|
||||
elif operator == 'starts_with':
|
||||
return field_value.startswith(pattern)
|
||||
elif operator == 'ends_with':
|
||||
return field_value.endswith(pattern)
|
||||
else:
|
||||
# Unknown operator
|
||||
return False
|
||||
|
||||
def _extract_field(self, field: str, tool_name: str,
|
||||
tool_input: Dict[str, Any], input_data: Dict[str, Any] = None) -> Optional[str]:
|
||||
"""Extract field value from tool input or hook input data.
|
||||
|
||||
Args:
|
||||
field: Field name like "command", "new_text", "file_path", "reason", "transcript"
|
||||
tool_name: Tool being used (may be empty for Stop events)
|
||||
tool_input: Tool input dict
|
||||
input_data: Full hook input (for accessing transcript_path, reason, etc.)
|
||||
|
||||
Returns:
|
||||
Field value as string, or None if not found
|
||||
"""
|
||||
# Direct tool_input fields
|
||||
if field in tool_input:
|
||||
value = tool_input[field]
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return str(value)
|
||||
|
||||
# For Stop events and other non-tool events, check input_data
|
||||
if input_data:
|
||||
# Stop event specific fields
|
||||
if field == 'reason':
|
||||
return input_data.get('reason', '')
|
||||
elif field == 'transcript':
|
||||
# Read transcript file if path provided
|
||||
transcript_path = input_data.get('transcript_path')
|
||||
if transcript_path:
|
||||
try:
|
||||
with open(transcript_path, 'r') as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
print(f"Warning: Transcript file not found: {transcript_path}", file=sys.stderr)
|
||||
return ''
|
||||
except PermissionError:
|
||||
print(f"Warning: Permission denied reading transcript: {transcript_path}", file=sys.stderr)
|
||||
return ''
|
||||
except (IOError, OSError) as e:
|
||||
print(f"Warning: Error reading transcript {transcript_path}: {e}", file=sys.stderr)
|
||||
return ''
|
||||
except UnicodeDecodeError as e:
|
||||
print(f"Warning: Encoding error in transcript {transcript_path}: {e}", file=sys.stderr)
|
||||
return ''
|
||||
elif field == 'user_prompt':
|
||||
# For UserPromptSubmit events
|
||||
return input_data.get('user_prompt', '')
|
||||
|
||||
# Handle special cases by tool type
|
||||
if tool_name == 'Bash':
|
||||
if field == 'command':
|
||||
return tool_input.get('command', '')
|
||||
|
||||
elif tool_name in ['Write', 'Edit']:
|
||||
if field == 'content':
|
||||
# Write uses 'content', Edit has 'new_string'
|
||||
return tool_input.get('content') or tool_input.get('new_string', '')
|
||||
elif field == 'new_text' or field == 'new_string':
|
||||
return tool_input.get('new_string', '')
|
||||
elif field == 'old_text' or field == 'old_string':
|
||||
return tool_input.get('old_string', '')
|
||||
elif field == 'file_path':
|
||||
return tool_input.get('file_path', '')
|
||||
|
||||
elif tool_name == 'MultiEdit':
|
||||
if field == 'file_path':
|
||||
return tool_input.get('file_path', '')
|
||||
elif field in ['new_text', 'content']:
|
||||
# Concatenate all edits
|
||||
edits = tool_input.get('edits', [])
|
||||
return ' '.join(e.get('new_string', '') for e in edits)
|
||||
|
||||
return None
|
||||
|
||||
def _regex_match(self, pattern: str, text: str) -> bool:
|
||||
"""Check if pattern matches text using regex.
|
||||
|
||||
Args:
|
||||
pattern: Regex pattern
|
||||
text: Text to match against
|
||||
|
||||
Returns:
|
||||
True if pattern matches
|
||||
"""
|
||||
try:
|
||||
# Use cached compiled regex (LRU cache with max 128 patterns)
|
||||
regex = compile_regex(pattern)
|
||||
return bool(regex.search(text))
|
||||
|
||||
except re.error as e:
|
||||
print(f"Invalid regex pattern '{pattern}': {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
# For testing
|
||||
if __name__ == '__main__':
|
||||
from hookify.core.config_loader import Condition, Rule
|
||||
|
||||
# Test rule evaluation
|
||||
rule = Rule(
|
||||
name="test-rm",
|
||||
enabled=True,
|
||||
event="bash",
|
||||
conditions=[
|
||||
Condition(field="command", operator="regex_match", pattern=r"rm\s+-rf")
|
||||
],
|
||||
message="Dangerous rm command!"
|
||||
)
|
||||
|
||||
engine = RuleEngine()
|
||||
|
||||
# Test matching input
|
||||
test_input = {
|
||||
"tool_name": "Bash",
|
||||
"tool_input": {
|
||||
"command": "rm -rf /tmp/test"
|
||||
}
|
||||
}
|
||||
|
||||
result = engine.evaluate_rules([rule], test_input)
|
||||
print("Match result:", result)
|
||||
|
||||
# Test non-matching input
|
||||
test_input2 = {
|
||||
"tool_name": "Bash",
|
||||
"tool_input": {
|
||||
"command": "ls -la"
|
||||
}
|
||||
}
|
||||
|
||||
result2 = engine.evaluate_rules([rule], test_input2)
|
||||
print("Non-match result:", result2)
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: warn-console-log
|
||||
enabled: true
|
||||
event: file
|
||||
pattern: console\.log\(
|
||||
action: warn
|
||||
---
|
||||
|
||||
🔍 **Console.log detected**
|
||||
|
||||
You're adding a console.log statement. Please consider:
|
||||
- Is this for debugging or should it be proper logging?
|
||||
- Will this ship to production?
|
||||
- Should this use a logging library instead?
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: block-dangerous-rm
|
||||
enabled: true
|
||||
event: bash
|
||||
pattern: rm\s+-rf
|
||||
action: block
|
||||
---
|
||||
|
||||
⚠️ **Dangerous rm command detected!**
|
||||
|
||||
This command could delete important files. Please:
|
||||
- Verify the path is correct
|
||||
- Consider using a safer approach
|
||||
- Make sure you have backups
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: require-tests-run
|
||||
enabled: false
|
||||
event: stop
|
||||
action: block
|
||||
conditions:
|
||||
- field: transcript
|
||||
operator: not_contains
|
||||
pattern: npm test|pytest|cargo test
|
||||
---
|
||||
|
||||
**Tests not detected in transcript!**
|
||||
|
||||
Before stopping, please run tests to verify your changes work correctly.
|
||||
|
||||
Look for test commands like:
|
||||
- `npm test`
|
||||
- `pytest`
|
||||
- `cargo test`
|
||||
|
||||
**Note:** This rule blocks stopping if no test commands appear in the transcript.
|
||||
Enable this rule only when you want strict test enforcement.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: warn-sensitive-files
|
||||
enabled: true
|
||||
event: file
|
||||
action: warn
|
||||
conditions:
|
||||
- field: file_path
|
||||
operator: regex_match
|
||||
pattern: \.env$|\.env\.|credentials|secrets
|
||||
---
|
||||
|
||||
🔐 **Sensitive file detected**
|
||||
|
||||
You're editing a file that may contain sensitive data:
|
||||
- Ensure credentials are not hardcoded
|
||||
- Use environment variables for secrets
|
||||
- Verify this file is in .gitignore
|
||||
- Consider using a secrets manager
|
||||
Executable
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"description": "Hookify plugin - User-configurable hooks from .local.md files",
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.py",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/posttooluse.py",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop.py",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/userpromptsubmit.py",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PostToolUse hook executor for hookify plugin.
|
||||
|
||||
This script is called by Claude Code after a tool executes.
|
||||
It reads .claude/hookify.*.local.md files and evaluates rules.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# CRITICAL: Add plugin root to Python path for imports
|
||||
PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
|
||||
if PLUGIN_ROOT:
|
||||
parent_dir = os.path.dirname(PLUGIN_ROOT)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.insert(0, parent_dir)
|
||||
if PLUGIN_ROOT not in sys.path:
|
||||
sys.path.insert(0, PLUGIN_ROOT)
|
||||
|
||||
try:
|
||||
from hookify.core.config_loader import load_rules
|
||||
from hookify.core.rule_engine import RuleEngine
|
||||
except ImportError as e:
|
||||
error_msg = {"systemMessage": f"Hookify import error: {e}"}
|
||||
print(json.dumps(error_msg), file=sys.stdout)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for PostToolUse hook."""
|
||||
try:
|
||||
# Read input from stdin
|
||||
input_data = json.load(sys.stdin)
|
||||
|
||||
# Determine event type based on tool
|
||||
tool_name = input_data.get('tool_name', '')
|
||||
event = None
|
||||
if tool_name == 'Bash':
|
||||
event = 'bash'
|
||||
elif tool_name in ['Edit', 'Write', 'MultiEdit']:
|
||||
event = 'file'
|
||||
|
||||
# Load rules
|
||||
rules = load_rules(event=event)
|
||||
|
||||
# Evaluate rules
|
||||
engine = RuleEngine()
|
||||
result = engine.evaluate_rules(rules, input_data)
|
||||
|
||||
# Always output JSON (even if empty)
|
||||
print(json.dumps(result), file=sys.stdout)
|
||||
|
||||
except Exception as e:
|
||||
error_output = {
|
||||
"systemMessage": f"Hookify error: {str(e)}"
|
||||
}
|
||||
print(json.dumps(error_output), file=sys.stdout)
|
||||
|
||||
finally:
|
||||
# ALWAYS exit 0
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PreToolUse hook executor for hookify plugin.
|
||||
|
||||
This script is called by Claude Code before any tool executes.
|
||||
It reads .claude/hookify.*.local.md files and evaluates rules.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# CRITICAL: Add plugin root to Python path for imports
|
||||
# We need to add the parent of the plugin directory so Python can find "hookify" package
|
||||
PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
|
||||
if PLUGIN_ROOT:
|
||||
# Add the parent directory of the plugin
|
||||
parent_dir = os.path.dirname(PLUGIN_ROOT)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
# Also add PLUGIN_ROOT itself in case we have other scripts
|
||||
if PLUGIN_ROOT not in sys.path:
|
||||
sys.path.insert(0, PLUGIN_ROOT)
|
||||
|
||||
try:
|
||||
from hookify.core.config_loader import load_rules
|
||||
from hookify.core.rule_engine import RuleEngine
|
||||
except ImportError as e:
|
||||
# If imports fail, allow operation and log error
|
||||
error_msg = {"systemMessage": f"Hookify import error: {e}"}
|
||||
print(json.dumps(error_msg), file=sys.stdout)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for PreToolUse hook."""
|
||||
try:
|
||||
# Read input from stdin
|
||||
input_data = json.load(sys.stdin)
|
||||
|
||||
# Determine event type for filtering
|
||||
# For PreToolUse, we use tool_name to determine "bash" vs "file" event
|
||||
tool_name = input_data.get('tool_name', '')
|
||||
|
||||
event = None
|
||||
if tool_name == 'Bash':
|
||||
event = 'bash'
|
||||
elif tool_name in ['Edit', 'Write', 'MultiEdit']:
|
||||
event = 'file'
|
||||
|
||||
# Load rules
|
||||
rules = load_rules(event=event)
|
||||
|
||||
# Evaluate rules
|
||||
engine = RuleEngine()
|
||||
result = engine.evaluate_rules(rules, input_data)
|
||||
|
||||
# Always output JSON (even if empty)
|
||||
print(json.dumps(result), file=sys.stdout)
|
||||
|
||||
except Exception as e:
|
||||
# On any error, allow the operation and log
|
||||
error_output = {
|
||||
"systemMessage": f"Hookify error: {str(e)}"
|
||||
}
|
||||
print(json.dumps(error_output), file=sys.stdout)
|
||||
|
||||
finally:
|
||||
# ALWAYS exit 0 - never block operations due to hook errors
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stop hook executor for hookify plugin.
|
||||
|
||||
This script is called by Claude Code when agent wants to stop.
|
||||
It reads .claude/hookify.*.local.md files and evaluates stop rules.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# CRITICAL: Add plugin root to Python path for imports
|
||||
PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
|
||||
if PLUGIN_ROOT:
|
||||
parent_dir = os.path.dirname(PLUGIN_ROOT)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.insert(0, parent_dir)
|
||||
if PLUGIN_ROOT not in sys.path:
|
||||
sys.path.insert(0, PLUGIN_ROOT)
|
||||
|
||||
try:
|
||||
from hookify.core.config_loader import load_rules
|
||||
from hookify.core.rule_engine import RuleEngine
|
||||
except ImportError as e:
|
||||
error_msg = {"systemMessage": f"Hookify import error: {e}"}
|
||||
print(json.dumps(error_msg), file=sys.stdout)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for Stop hook."""
|
||||
try:
|
||||
# Read input from stdin
|
||||
input_data = json.load(sys.stdin)
|
||||
|
||||
# Load stop rules
|
||||
rules = load_rules(event='stop')
|
||||
|
||||
# Evaluate rules
|
||||
engine = RuleEngine()
|
||||
result = engine.evaluate_rules(rules, input_data)
|
||||
|
||||
# Always output JSON (even if empty)
|
||||
print(json.dumps(result), file=sys.stdout)
|
||||
|
||||
except Exception as e:
|
||||
# On any error, allow the operation
|
||||
error_output = {
|
||||
"systemMessage": f"Hookify error: {str(e)}"
|
||||
}
|
||||
print(json.dumps(error_output), file=sys.stdout)
|
||||
|
||||
finally:
|
||||
# ALWAYS exit 0
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""UserPromptSubmit hook executor for hookify plugin.
|
||||
|
||||
This script is called by Claude Code when user submits a prompt.
|
||||
It reads .claude/hookify.*.local.md files and evaluates rules.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# CRITICAL: Add plugin root to Python path for imports
|
||||
PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
|
||||
if PLUGIN_ROOT:
|
||||
parent_dir = os.path.dirname(PLUGIN_ROOT)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.insert(0, parent_dir)
|
||||
if PLUGIN_ROOT not in sys.path:
|
||||
sys.path.insert(0, PLUGIN_ROOT)
|
||||
|
||||
try:
|
||||
from hookify.core.config_loader import load_rules
|
||||
from hookify.core.rule_engine import RuleEngine
|
||||
except ImportError as e:
|
||||
error_msg = {"systemMessage": f"Hookify import error: {e}"}
|
||||
print(json.dumps(error_msg), file=sys.stdout)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for UserPromptSubmit hook."""
|
||||
try:
|
||||
# Read input from stdin
|
||||
input_data = json.load(sys.stdin)
|
||||
|
||||
# Load user prompt rules
|
||||
rules = load_rules(event='prompt')
|
||||
|
||||
# Evaluate rules
|
||||
engine = RuleEngine()
|
||||
result = engine.evaluate_rules(rules, input_data)
|
||||
|
||||
# Always output JSON (even if empty)
|
||||
print(json.dumps(result), file=sys.stdout)
|
||||
|
||||
except Exception as e:
|
||||
error_output = {
|
||||
"systemMessage": f"Hookify error: {str(e)}"
|
||||
}
|
||||
print(json.dumps(error_output), file=sys.stdout)
|
||||
|
||||
finally:
|
||||
# ALWAYS exit 0
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,374 @@
|
||||
---
|
||||
name: Writing Hookify Rules
|
||||
description: This skill should be used when the user asks to "create a hookify rule", "write a hook rule", "configure hookify", "add a hookify rule", or needs guidance on hookify rule syntax and patterns.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Writing Hookify Rules
|
||||
|
||||
## Overview
|
||||
|
||||
Hookify rules are markdown files with YAML frontmatter that define patterns to watch for and messages to show when those patterns match. Rules are stored in `.claude/hookify.{rule-name}.local.md` files.
|
||||
|
||||
## Rule File Format
|
||||
|
||||
### Basic Structure
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: rule-identifier
|
||||
enabled: true
|
||||
event: bash|file|stop|prompt|all
|
||||
pattern: regex-pattern-here
|
||||
---
|
||||
|
||||
Message to show Claude when this rule triggers.
|
||||
Can include markdown formatting, warnings, suggestions, etc.
|
||||
```
|
||||
|
||||
### Frontmatter Fields
|
||||
|
||||
**name** (required): Unique identifier for the rule
|
||||
- Use kebab-case: `warn-dangerous-rm`, `block-console-log`
|
||||
- Be descriptive and action-oriented
|
||||
- Start with verb: warn, prevent, block, require, check
|
||||
|
||||
**enabled** (required): Boolean to activate/deactivate
|
||||
- `true`: Rule is active
|
||||
- `false`: Rule is disabled (won't trigger)
|
||||
- Can toggle without deleting rule
|
||||
|
||||
**event** (required): Which hook event to trigger on
|
||||
- `bash`: Bash tool commands
|
||||
- `file`: Edit, Write, MultiEdit tools
|
||||
- `stop`: When agent wants to stop
|
||||
- `prompt`: When user submits a prompt
|
||||
- `all`: All events
|
||||
|
||||
**action** (optional): What to do when rule matches
|
||||
- `warn`: Show message but allow operation (default)
|
||||
- `block`: Prevent operation (PreToolUse) or stop session (Stop events)
|
||||
- If omitted, defaults to `warn`
|
||||
|
||||
**pattern** (simple format): Regex pattern to match
|
||||
- Used for simple single-condition rules
|
||||
- Matches against command (bash) or new_text (file)
|
||||
- Python regex syntax
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
event: bash
|
||||
pattern: rm\s+-rf
|
||||
```
|
||||
|
||||
### Advanced Format (Multiple Conditions)
|
||||
|
||||
For complex rules with multiple conditions:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: warn-env-file-edits
|
||||
enabled: true
|
||||
event: file
|
||||
conditions:
|
||||
- field: file_path
|
||||
operator: regex_match
|
||||
pattern: \.env$
|
||||
- field: new_text
|
||||
operator: contains
|
||||
pattern: API_KEY
|
||||
---
|
||||
|
||||
You're adding an API key to a .env file. Ensure this file is in .gitignore!
|
||||
```
|
||||
|
||||
**Condition fields:**
|
||||
- `field`: Which field to check
|
||||
- For bash: `command`
|
||||
- For file: `file_path`, `new_text`, `old_text`, `content`
|
||||
- `operator`: How to match
|
||||
- `regex_match`: Regex pattern matching
|
||||
- `contains`: Substring check
|
||||
- `equals`: Exact match
|
||||
- `not_contains`: Substring must NOT be present
|
||||
- `starts_with`: Prefix check
|
||||
- `ends_with`: Suffix check
|
||||
- `pattern`: Pattern or string to match
|
||||
|
||||
**All conditions must match for rule to trigger.**
|
||||
|
||||
## Message Body
|
||||
|
||||
The markdown content after frontmatter is shown to Claude when the rule triggers.
|
||||
|
||||
**Good messages:**
|
||||
- Explain what was detected
|
||||
- Explain why it's problematic
|
||||
- Suggest alternatives or best practices
|
||||
- Use formatting for clarity (bold, lists, etc.)
|
||||
|
||||
**Example:**
|
||||
```markdown
|
||||
⚠️ **Console.log detected!**
|
||||
|
||||
You're adding console.log to production code.
|
||||
|
||||
**Why this matters:**
|
||||
- Debug logs shouldn't ship to production
|
||||
- Console.log can expose sensitive data
|
||||
- Impacts browser performance
|
||||
|
||||
**Alternatives:**
|
||||
- Use a proper logging library
|
||||
- Remove before committing
|
||||
- Use conditional debug builds
|
||||
```
|
||||
|
||||
## Event Type Guide
|
||||
|
||||
### bash Events
|
||||
|
||||
Match Bash command patterns:
|
||||
|
||||
```markdown
|
||||
---
|
||||
event: bash
|
||||
pattern: sudo\s+|rm\s+-rf|chmod\s+777
|
||||
---
|
||||
|
||||
Dangerous command detected!
|
||||
```
|
||||
|
||||
**Common patterns:**
|
||||
- Dangerous commands: `rm\s+-rf`, `dd\s+if=`, `mkfs`
|
||||
- Privilege escalation: `sudo\s+`, `su\s+`
|
||||
- Permission issues: `chmod\s+777`, `chown\s+root`
|
||||
|
||||
### file Events
|
||||
|
||||
Match Edit/Write/MultiEdit operations:
|
||||
|
||||
```markdown
|
||||
---
|
||||
event: file
|
||||
pattern: console\.log\(|eval\(|innerHTML\s*=
|
||||
---
|
||||
|
||||
Potentially problematic code pattern detected!
|
||||
```
|
||||
|
||||
**Match on different fields:**
|
||||
```markdown
|
||||
---
|
||||
event: file
|
||||
conditions:
|
||||
- field: file_path
|
||||
operator: regex_match
|
||||
pattern: \.tsx?$
|
||||
- field: new_text
|
||||
operator: regex_match
|
||||
pattern: console\.log\(
|
||||
---
|
||||
|
||||
Console.log in TypeScript file!
|
||||
```
|
||||
|
||||
**Common patterns:**
|
||||
- Debug code: `console\.log\(`, `debugger`, `print\(`
|
||||
- Security risks: `eval\(`, `innerHTML\s*=`, `dangerouslySetInnerHTML`
|
||||
- Sensitive files: `\.env$`, `credentials`, `\.pem$`
|
||||
- Generated files: `node_modules/`, `dist/`, `build/`
|
||||
|
||||
### stop Events
|
||||
|
||||
Match when agent wants to stop (completion checks):
|
||||
|
||||
```markdown
|
||||
---
|
||||
event: stop
|
||||
pattern: .*
|
||||
---
|
||||
|
||||
Before stopping, verify:
|
||||
- [ ] Tests were run
|
||||
- [ ] Build succeeded
|
||||
- [ ] Documentation updated
|
||||
```
|
||||
|
||||
**Use for:**
|
||||
- Reminders about required steps
|
||||
- Completion checklists
|
||||
- Process enforcement
|
||||
|
||||
### prompt Events
|
||||
|
||||
Match user prompt content (advanced):
|
||||
|
||||
```markdown
|
||||
---
|
||||
event: prompt
|
||||
conditions:
|
||||
- field: user_prompt
|
||||
operator: contains
|
||||
pattern: deploy to production
|
||||
---
|
||||
|
||||
Production deployment checklist:
|
||||
- [ ] Tests passing?
|
||||
- [ ] Reviewed by team?
|
||||
- [ ] Monitoring ready?
|
||||
```
|
||||
|
||||
## Pattern Writing Tips
|
||||
|
||||
### Regex Basics
|
||||
|
||||
**Literal characters:** Most characters match themselves
|
||||
- `rm` matches "rm"
|
||||
- `console.log` matches "console.log"
|
||||
|
||||
**Special characters need escaping:**
|
||||
- `.` (any char) → `\.` (literal dot)
|
||||
- `(` `)` → `\(` `\)` (literal parens)
|
||||
- `[` `]` → `\[` `\]` (literal brackets)
|
||||
|
||||
**Common metacharacters:**
|
||||
- `\s` - whitespace (space, tab, newline)
|
||||
- `\d` - digit (0-9)
|
||||
- `\w` - word character (a-z, A-Z, 0-9, _)
|
||||
- `.` - any character
|
||||
- `+` - one or more
|
||||
- `*` - zero or more
|
||||
- `?` - zero or one
|
||||
- `|` - OR
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
rm\s+-rf Matches: rm -rf, rm -rf
|
||||
console\.log\( Matches: console.log(
|
||||
(eval|exec)\( Matches: eval( or exec(
|
||||
chmod\s+777 Matches: chmod 777, chmod 777
|
||||
API_KEY\s*= Matches: API_KEY=, API_KEY =
|
||||
```
|
||||
|
||||
### Testing Patterns
|
||||
|
||||
Test regex patterns before using:
|
||||
|
||||
```bash
|
||||
python3 -c "import re; print(re.search(r'your_pattern', 'test text'))"
|
||||
```
|
||||
|
||||
Or use online regex testers (regex101.com with Python flavor).
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
**Too broad:**
|
||||
```yaml
|
||||
pattern: log # Matches "log", "login", "dialog", "catalog"
|
||||
```
|
||||
Better: `console\.log\(|logger\.`
|
||||
|
||||
**Too specific:**
|
||||
```yaml
|
||||
pattern: rm -rf /tmp # Only matches exact path
|
||||
```
|
||||
Better: `rm\s+-rf`
|
||||
|
||||
**Escaping issues:**
|
||||
- YAML quoted strings: `"pattern"` requires double backslashes `\\s`
|
||||
- YAML unquoted: `pattern: \s` works as-is
|
||||
- **Recommendation**: Use unquoted patterns in YAML
|
||||
|
||||
## File Organization
|
||||
|
||||
**Location:** All rules in `.claude/` directory
|
||||
**Naming:** `.claude/hookify.{descriptive-name}.local.md`
|
||||
**Gitignore:** Add `.claude/*.local.md` to `.gitignore`
|
||||
|
||||
**Good names:**
|
||||
- `hookify.dangerous-rm.local.md`
|
||||
- `hookify.console-log.local.md`
|
||||
- `hookify.require-tests.local.md`
|
||||
- `hookify.sensitive-files.local.md`
|
||||
|
||||
**Bad names:**
|
||||
- `hookify.rule1.local.md` (not descriptive)
|
||||
- `hookify.md` (missing .local)
|
||||
- `danger.local.md` (missing hookify prefix)
|
||||
|
||||
## Workflow
|
||||
|
||||
### Creating a Rule
|
||||
|
||||
1. Identify unwanted behavior
|
||||
2. Determine which tool is involved (Bash, Edit, etc.)
|
||||
3. Choose event type (bash, file, stop, etc.)
|
||||
4. Write regex pattern
|
||||
5. Create `.claude/hookify.{name}.local.md` file in project root
|
||||
6. Test immediately - rules are read dynamically on next tool use
|
||||
|
||||
### Refining a Rule
|
||||
|
||||
1. Edit the `.local.md` file
|
||||
2. Adjust pattern or message
|
||||
3. Test immediately - changes take effect on next tool use
|
||||
|
||||
### Disabling a Rule
|
||||
|
||||
**Temporary:** Set `enabled: false` in frontmatter
|
||||
**Permanent:** Delete the `.local.md` file
|
||||
|
||||
## Examples
|
||||
|
||||
See `${CLAUDE_PLUGIN_ROOT}/examples/` for complete examples:
|
||||
- `dangerous-rm.local.md` - Block dangerous rm commands
|
||||
- `console-log-warning.local.md` - Warn about console.log
|
||||
- `sensitive-files-warning.local.md` - Warn about editing .env files
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Minimum viable rule:**
|
||||
```markdown
|
||||
---
|
||||
name: my-rule
|
||||
enabled: true
|
||||
event: bash
|
||||
pattern: dangerous_command
|
||||
---
|
||||
|
||||
Warning message here
|
||||
```
|
||||
|
||||
**Rule with conditions:**
|
||||
```markdown
|
||||
---
|
||||
name: my-rule
|
||||
enabled: true
|
||||
event: file
|
||||
conditions:
|
||||
- field: file_path
|
||||
operator: regex_match
|
||||
pattern: \.ts$
|
||||
- field: new_text
|
||||
operator: contains
|
||||
pattern: any
|
||||
---
|
||||
|
||||
Warning message
|
||||
```
|
||||
|
||||
**Event types:**
|
||||
- `bash` - Bash commands
|
||||
- `file` - File edits
|
||||
- `stop` - Completion checks
|
||||
- `prompt` - User input
|
||||
- `all` - All events
|
||||
|
||||
**Field options:**
|
||||
- Bash: `command`
|
||||
- File: `file_path`, `new_text`, `old_text`, `content`
|
||||
- Prompt: `user_prompt`
|
||||
|
||||
**Operators:**
|
||||
- `regex_match`, `contains`, `equals`, `not_contains`, `starts_with`, `ends_with`
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Output the learning mode instructions as additionalContext
|
||||
# This combines the unshipped Learning output style with explanatory functionality
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
# Plugin Development Toolkit
|
||||
|
||||
A comprehensive toolkit for developing Claude Code plugins with expert guidance on hooks, MCP integration, plugin structure, and marketplace publishing.
|
||||
|
||||
## Overview
|
||||
|
||||
The plugin-dev toolkit provides seven specialized skills to help you build high-quality Claude Code plugins:
|
||||
|
||||
1. **Hook Development** - Advanced hooks API and event-driven automation
|
||||
2. **MCP Integration** - Model Context Protocol server integration
|
||||
3. **Plugin Structure** - Plugin organization and manifest configuration
|
||||
4. **Plugin Settings** - Configuration patterns using .claude/plugin-name.local.md files
|
||||
5. **Command Development** - Creating slash commands with frontmatter and arguments
|
||||
6. **Agent Development** - Creating autonomous agents with AI-assisted generation
|
||||
7. **Skill Development** - Creating skills with progressive disclosure and strong triggers
|
||||
|
||||
Each skill follows best practices with progressive disclosure: lean core documentation, detailed references, working examples, and utility scripts.
|
||||
|
||||
## Guided Workflow Command
|
||||
|
||||
### /plugin-dev:create-plugin
|
||||
|
||||
A comprehensive, end-to-end workflow command for creating plugins from scratch, similar to the feature-dev workflow.
|
||||
|
||||
**8-Phase Process:**
|
||||
1. **Discovery** - Understand plugin purpose and requirements
|
||||
2. **Component Planning** - Determine needed skills, commands, agents, hooks, MCP
|
||||
3. **Detailed Design** - Specify each component and resolve ambiguities
|
||||
4. **Structure Creation** - Set up directories and manifest
|
||||
5. **Component Implementation** - Create each component using AI-assisted agents
|
||||
6. **Validation** - Run plugin-validator and component-specific checks
|
||||
7. **Testing** - Verify plugin works in Claude Code
|
||||
8. **Documentation** - Finalize README and prepare for distribution
|
||||
|
||||
**Features:**
|
||||
- Asks clarifying questions at each phase
|
||||
- Loads relevant skills automatically
|
||||
- Uses agent-creator for AI-assisted agent generation
|
||||
- Runs validation utilities (validate-agent.sh, validate-hook-schema.sh, etc.)
|
||||
- Follows plugin-dev's own proven patterns
|
||||
- Guides through testing and verification
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
/plugin-dev:create-plugin [optional description]
|
||||
|
||||
# Examples:
|
||||
/plugin-dev:create-plugin
|
||||
/plugin-dev:create-plugin A plugin for managing database migrations
|
||||
```
|
||||
|
||||
Use this workflow for structured, high-quality plugin development from concept to completion.
|
||||
|
||||
## Skills
|
||||
|
||||
### 1. Hook Development
|
||||
|
||||
**Trigger phrases:** "create a hook", "add a PreToolUse hook", "validate tool use", "implement prompt-based hooks", "${CLAUDE_PLUGIN_ROOT}", "block dangerous commands"
|
||||
|
||||
**What it covers:**
|
||||
- Prompt-based hooks (recommended) with LLM decision-making
|
||||
- Command hooks for deterministic validation
|
||||
- All hook events: PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification
|
||||
- Hook output formats and JSON schemas
|
||||
- Security best practices and input validation
|
||||
- ${CLAUDE_PLUGIN_ROOT} for portable paths
|
||||
|
||||
**Resources:**
|
||||
- Core SKILL.md (1,619 words)
|
||||
- 3 example hook scripts (validate-write, validate-bash, load-context)
|
||||
- 3 reference docs: patterns, migration, advanced techniques
|
||||
- 3 utility scripts: validate-hook-schema.sh, test-hook.sh, hook-linter.sh
|
||||
|
||||
**Use when:** Creating event-driven automation, validating operations, or enforcing policies in your plugin.
|
||||
|
||||
### 2. MCP Integration
|
||||
|
||||
**Trigger phrases:** "add MCP server", "integrate MCP", "configure .mcp.json", "Model Context Protocol", "stdio/SSE/HTTP server", "connect external service"
|
||||
|
||||
**What it covers:**
|
||||
- MCP server configuration (.mcp.json vs plugin.json)
|
||||
- All server types: stdio (local), SSE (hosted/OAuth), HTTP (REST), WebSocket (real-time)
|
||||
- Environment variable expansion (${CLAUDE_PLUGIN_ROOT}, user vars)
|
||||
- MCP tool naming and usage in commands/agents
|
||||
- Authentication patterns: OAuth, tokens, env vars
|
||||
- Integration patterns and performance optimization
|
||||
|
||||
**Resources:**
|
||||
- Core SKILL.md (1,666 words)
|
||||
- 3 example configurations (stdio, SSE, HTTP)
|
||||
- 3 reference docs: server-types (~3,200w), authentication (~2,800w), tool-usage (~2,600w)
|
||||
|
||||
**Use when:** Integrating external services, APIs, databases, or tools into your plugin.
|
||||
|
||||
### 3. Plugin Structure
|
||||
|
||||
**Trigger phrases:** "plugin structure", "plugin.json manifest", "auto-discovery", "component organization", "plugin directory layout"
|
||||
|
||||
**What it covers:**
|
||||
- Standard plugin directory structure and auto-discovery
|
||||
- plugin.json manifest format and all fields
|
||||
- Component organization (commands, agents, skills, hooks)
|
||||
- ${CLAUDE_PLUGIN_ROOT} usage throughout
|
||||
- File naming conventions and best practices
|
||||
- Minimal, standard, and advanced plugin patterns
|
||||
|
||||
**Resources:**
|
||||
- Core SKILL.md (1,619 words)
|
||||
- 3 example structures (minimal, standard, advanced)
|
||||
- 2 reference docs: component-patterns, manifest-reference
|
||||
|
||||
**Use when:** Starting a new plugin, organizing components, or configuring the plugin manifest.
|
||||
|
||||
### 4. Plugin Settings
|
||||
|
||||
**Trigger phrases:** "plugin settings", "store plugin configuration", ".local.md files", "plugin state files", "read YAML frontmatter", "per-project plugin settings"
|
||||
|
||||
**What it covers:**
|
||||
- .claude/plugin-name.local.md pattern for configuration
|
||||
- YAML frontmatter + markdown body structure
|
||||
- Parsing techniques for bash scripts (sed, awk, grep patterns)
|
||||
- Temporarily active hooks (flag files and quick-exit)
|
||||
- Real-world examples from multi-agent-swarm and ralph-wiggum plugins
|
||||
- Atomic file updates and validation
|
||||
- Gitignore and lifecycle management
|
||||
|
||||
**Resources:**
|
||||
- Core SKILL.md (1,623 words)
|
||||
- 3 examples (read-settings hook, create-settings command, templates)
|
||||
- 2 reference docs: parsing-techniques, real-world-examples
|
||||
- 2 utility scripts: validate-settings.sh, parse-frontmatter.sh
|
||||
|
||||
**Use when:** Making plugins configurable, storing per-project state, or implementing user preferences.
|
||||
|
||||
### 5. Command Development
|
||||
|
||||
**Trigger phrases:** "create a slash command", "add a command", "command frontmatter", "define command arguments", "organize commands"
|
||||
|
||||
**What it covers:**
|
||||
- Slash command structure and markdown format
|
||||
- YAML frontmatter fields (description, argument-hint, allowed-tools)
|
||||
- Dynamic arguments and file references
|
||||
- Bash execution for context
|
||||
- Command organization and namespacing
|
||||
- Best practices for command development
|
||||
|
||||
**Resources:**
|
||||
- Core SKILL.md (1,535 words)
|
||||
- Examples and reference documentation
|
||||
- Command organization patterns
|
||||
|
||||
**Use when:** Creating slash commands, defining command arguments, or organizing plugin commands.
|
||||
|
||||
### 6. Agent Development
|
||||
|
||||
**Trigger phrases:** "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "autonomous agent"
|
||||
|
||||
**What it covers:**
|
||||
- Agent file structure (YAML frontmatter + system prompt)
|
||||
- All frontmatter fields (name, description, model, color, tools)
|
||||
- Description format with <example> blocks for reliable triggering
|
||||
- System prompt design patterns (analysis, generation, validation, orchestration)
|
||||
- AI-assisted agent generation using Claude Code's proven prompt
|
||||
- Validation rules and best practices
|
||||
- Complete production-ready agent examples
|
||||
|
||||
**Resources:**
|
||||
- Core SKILL.md (1,438 words)
|
||||
- 2 examples: agent-creation-prompt (AI-assisted workflow), complete-agent-examples (4 full agents)
|
||||
- 3 reference docs: agent-creation-system-prompt (from Claude Code), system-prompt-design (~4,000w), triggering-examples (~2,500w)
|
||||
- 1 utility script: validate-agent.sh
|
||||
|
||||
**Use when:** Creating autonomous agents, defining agent behavior, or implementing AI-assisted agent generation.
|
||||
|
||||
### 7. Skill Development
|
||||
|
||||
**Trigger phrases:** "create a skill", "add a skill to plugin", "write a new skill", "improve skill description", "organize skill content"
|
||||
|
||||
**What it covers:**
|
||||
- Skill structure (SKILL.md with YAML frontmatter)
|
||||
- Progressive disclosure principle (metadata → SKILL.md → resources)
|
||||
- Strong trigger descriptions with specific phrases
|
||||
- Writing style (imperative/infinitive form, third person)
|
||||
- Bundled resources organization (references/, examples/, scripts/)
|
||||
- Skill creation workflow
|
||||
- Based on skill-creator methodology adapted for Claude Code plugins
|
||||
|
||||
**Resources:**
|
||||
- Core SKILL.md (1,232 words)
|
||||
- References: skill-creator methodology, plugin-dev patterns
|
||||
- Examples: Study plugin-dev's own skills as templates
|
||||
|
||||
**Use when:** Creating new skills for plugins or improving existing skill quality.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Install from claude-code-marketplace:
|
||||
|
||||
```bash
|
||||
/plugin install plugin-dev@claude-code-marketplace
|
||||
```
|
||||
|
||||
Or for development, use directly:
|
||||
|
||||
```bash
|
||||
cc --plugin-dir /path/to/plugin-dev
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Creating Your First Plugin
|
||||
|
||||
1. **Plan your plugin structure:**
|
||||
- Ask: "What's the best directory structure for a plugin with commands and MCP integration?"
|
||||
- The plugin-structure skill will guide you
|
||||
|
||||
2. **Add MCP integration (if needed):**
|
||||
- Ask: "How do I add an MCP server for database access?"
|
||||
- The mcp-integration skill provides examples and patterns
|
||||
|
||||
3. **Implement hooks (if needed):**
|
||||
- Ask: "Create a PreToolUse hook that validates file writes"
|
||||
- The hook-development skill gives working examples and utilities
|
||||
|
||||
|
||||
## Development Workflow
|
||||
|
||||
The plugin-dev toolkit supports your entire plugin development lifecycle:
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ Design Structure │ → plugin-structure skill
|
||||
│ (manifest, layout) │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ Add Components │
|
||||
│ (commands, agents, │ → All skills provide guidance
|
||||
│ skills, hooks) │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ Integrate Services │ → mcp-integration skill
|
||||
│ (MCP servers) │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ Add Automation │ → hook-development skill
|
||||
│ (hooks, validation)│ + utility scripts
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ Test & Validate │ → hook-development utilities
|
||||
│ │ validate-hook-schema.sh
|
||||
└──────────┬──────────┘ test-hook.sh
|
||||
│ hook-linter.sh
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Progressive Disclosure
|
||||
|
||||
Each skill uses a three-level disclosure system:
|
||||
1. **Metadata** (always loaded): Concise descriptions with strong triggers
|
||||
2. **Core SKILL.md** (when triggered): Essential API reference (~1,500-2,000 words)
|
||||
3. **References/Examples** (as needed): Detailed guides, patterns, and working code
|
||||
|
||||
This keeps Claude Code's context focused while providing deep knowledge when needed.
|
||||
|
||||
### Utility Scripts
|
||||
|
||||
The hook-development skill includes production-ready utilities:
|
||||
|
||||
```bash
|
||||
# Validate hooks.json structure
|
||||
./validate-hook-schema.sh hooks/hooks.json
|
||||
|
||||
# Test hooks before deployment
|
||||
./test-hook.sh my-hook.sh test-input.json
|
||||
|
||||
# Lint hook scripts for best practices
|
||||
./hook-linter.sh my-hook.sh
|
||||
```
|
||||
|
||||
### Working Examples
|
||||
|
||||
Every skill provides working examples:
|
||||
- **Hook Development**: 3 complete hook scripts (bash, write validation, context loading)
|
||||
- **MCP Integration**: 3 server configurations (stdio, SSE, HTTP)
|
||||
- **Plugin Structure**: 3 plugin layouts (minimal, standard, advanced)
|
||||
- **Plugin Settings**: 3 examples (read-settings hook, create-settings command, templates)
|
||||
- **Command Development**: 10 complete command examples (review, test, deploy, docs, etc.)
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
All skills follow consistent standards:
|
||||
- Third-person descriptions ("This skill should be used when...")
|
||||
- Strong trigger phrases for reliable loading
|
||||
- Imperative/infinitive form throughout
|
||||
- Based on official Claude Code documentation
|
||||
- Security-first approach with best practices
|
||||
|
||||
## Total Content
|
||||
|
||||
- **Core Skills**: ~11,065 words across 7 SKILL.md files
|
||||
- **Reference Docs**: ~10,000+ words of detailed guides
|
||||
- **Examples**: 12+ working examples (hook scripts, MCP configs, plugin layouts, settings files)
|
||||
- **Utilities**: 6 production-ready validation/testing/parsing scripts
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Building a Database Plugin
|
||||
|
||||
```
|
||||
1. "What's the structure for a plugin with MCP integration?"
|
||||
→ plugin-structure skill provides layout
|
||||
|
||||
2. "How do I configure an stdio MCP server for PostgreSQL?"
|
||||
→ mcp-integration skill shows configuration
|
||||
|
||||
3. "Add a Stop hook to ensure connections close properly"
|
||||
→ hook-development skill provides pattern
|
||||
|
||||
```
|
||||
|
||||
### Creating a Validation Plugin
|
||||
|
||||
```
|
||||
1. "Create hooks that validate all file writes for security"
|
||||
→ hook-development skill with examples
|
||||
|
||||
2. "Test my hooks before deploying"
|
||||
→ Use validate-hook-schema.sh and test-hook.sh
|
||||
|
||||
3. "Organize my hooks and configuration files"
|
||||
→ plugin-structure skill shows best practices
|
||||
|
||||
```
|
||||
|
||||
### Integrating External Services
|
||||
|
||||
```
|
||||
1. "Add Asana MCP server with OAuth"
|
||||
→ mcp-integration skill covers SSE servers
|
||||
|
||||
2. "Use Asana tools in my commands"
|
||||
→ mcp-integration tool-usage reference
|
||||
|
||||
3. "Structure my plugin with commands and MCP"
|
||||
→ plugin-structure skill provides patterns
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
All skills emphasize:
|
||||
|
||||
✅ **Security First**
|
||||
- Input validation in hooks
|
||||
- HTTPS/WSS for MCP servers
|
||||
- Environment variables for credentials
|
||||
- Principle of least privilege
|
||||
|
||||
✅ **Portability**
|
||||
- Use ${CLAUDE_PLUGIN_ROOT} everywhere
|
||||
- Relative paths only
|
||||
- Environment variable substitution
|
||||
|
||||
✅ **Testing**
|
||||
- Validate configurations before deployment
|
||||
- Test hooks with sample inputs
|
||||
- Use debug mode (`claude --debug`)
|
||||
|
||||
✅ **Documentation**
|
||||
- Clear README files
|
||||
- Documented environment variables
|
||||
- Usage examples
|
||||
|
||||
## Contributing
|
||||
|
||||
This plugin is part of the claude-code-marketplace. To contribute improvements:
|
||||
|
||||
1. Fork the marketplace repository
|
||||
2. Make changes to plugin-dev/
|
||||
3. Test locally with `cc --plugin-dir`
|
||||
4. Create PR following marketplace-publishing guidelines
|
||||
|
||||
## Version
|
||||
|
||||
0.1.0 - Initial release with seven comprehensive skills and three validation agents
|
||||
|
||||
## Author
|
||||
|
||||
Daisy Hollman (daisy@anthropic.com)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See repository for details
|
||||
|
||||
---
|
||||
|
||||
**Note:** This toolkit is designed to help you build high-quality plugins. The skills load automatically when you ask relevant questions, providing expert guidance exactly when you need it.
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
name: agent-creator
|
||||
description: Use this agent when the user asks to "create an agent", "generate an agent", "build a new agent", "make me an agent that...", or describes agent functionality they need. Trigger when user wants to create autonomous agents for plugins. Examples:
|
||||
|
||||
<example>
|
||||
Context: User wants to create a code review agent
|
||||
user: "Create an agent that reviews code for quality issues"
|
||||
assistant: "I'll use the agent-creator agent to generate the agent configuration."
|
||||
<commentary>
|
||||
User requesting new agent creation, trigger agent-creator to generate it.
|
||||
</commentary>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
Context: User describes needed functionality
|
||||
user: "I need an agent that generates unit tests for my code"
|
||||
assistant: "I'll use the agent-creator agent to create a test generation agent."
|
||||
<commentary>
|
||||
User describes agent need, trigger agent-creator to build it.
|
||||
</commentary>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
Context: User wants to add agent to plugin
|
||||
user: "Add an agent to my plugin that validates configurations"
|
||||
assistant: "I'll use the agent-creator agent to generate a configuration validator agent."
|
||||
<commentary>
|
||||
Plugin development with agent addition, trigger agent-creator.
|
||||
</commentary>
|
||||
</example>
|
||||
|
||||
model: sonnet
|
||||
color: magenta
|
||||
tools: ["Write", "Read"]
|
||||
---
|
||||
|
||||
You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability.
|
||||
|
||||
**Important Context**: You may have access to project-specific instructions from CLAUDE.md files and other context that may include coding standards, project structure, and custom requirements. Consider this context when creating agents to ensure they align with the project's established patterns and practices.
|
||||
|
||||
When a user describes what they want an agent to do, you will:
|
||||
|
||||
1. **Extract Core Intent**: Identify the fundamental purpose, key responsibilities, and success criteria for the agent. Look for both explicit requirements and implicit needs. Consider any project-specific context from CLAUDE.md files. For agents that are meant to review code, you should assume that the user is asking to review recently written code and not the whole codebase, unless the user has explicitly instructed you otherwise.
|
||||
|
||||
2. **Design Expert Persona**: Create a compelling expert identity that embodies deep domain knowledge relevant to the task. The persona should inspire confidence and guide the agent's decision-making approach.
|
||||
|
||||
3. **Architect Comprehensive Instructions**: Develop a system prompt that:
|
||||
- Establishes clear behavioral boundaries and operational parameters
|
||||
- Provides specific methodologies and best practices for task execution
|
||||
- Anticipates edge cases and provides guidance for handling them
|
||||
- Incorporates any specific requirements or preferences mentioned by the user
|
||||
- Defines output format expectations when relevant
|
||||
- Aligns with project-specific coding standards and patterns from CLAUDE.md
|
||||
|
||||
4. **Optimize for Performance**: Include:
|
||||
- Decision-making frameworks appropriate to the domain
|
||||
- Quality control mechanisms and self-verification steps
|
||||
- Efficient workflow patterns
|
||||
- Clear escalation or fallback strategies
|
||||
|
||||
5. **Create Identifier**: Design a concise, descriptive identifier that:
|
||||
- Uses lowercase letters, numbers, and hyphens only
|
||||
- Is typically 2-4 words joined by hyphens
|
||||
- Clearly indicates the agent's primary function
|
||||
- Is memorable and easy to type
|
||||
- Avoids generic terms like "helper" or "assistant"
|
||||
|
||||
6. **Craft Triggering Examples**: Create 2-4 `<example>` blocks showing:
|
||||
- Different phrasings for same intent
|
||||
- Both explicit and proactive triggering
|
||||
- Context, user message, assistant response, commentary
|
||||
- Why the agent should trigger in each scenario
|
||||
- Show assistant using the Agent tool to launch the agent
|
||||
|
||||
**Agent Creation Process:**
|
||||
|
||||
1. **Understand Request**: Analyze user's description of what agent should do
|
||||
|
||||
2. **Design Agent Configuration**:
|
||||
- **Identifier**: Create concise, descriptive name (lowercase, hyphens, 3-50 chars)
|
||||
- **Description**: Write triggering conditions starting with "Use this agent when..."
|
||||
- **Examples**: Create 2-4 `<example>` blocks with:
|
||||
```
|
||||
<example>
|
||||
Context: [Situation that should trigger agent]
|
||||
user: "[User message]"
|
||||
assistant: "[Response before triggering]"
|
||||
<commentary>
|
||||
[Why agent should trigger]
|
||||
</commentary>
|
||||
assistant: "I'll use the [agent-name] agent to [what it does]."
|
||||
</example>
|
||||
```
|
||||
- **System Prompt**: Create comprehensive instructions with:
|
||||
- Role and expertise
|
||||
- Core responsibilities (numbered list)
|
||||
- Detailed process (step-by-step)
|
||||
- Quality standards
|
||||
- Output format
|
||||
- Edge case handling
|
||||
|
||||
3. **Select Configuration**:
|
||||
- **Model**: Use `inherit` unless user specifies (sonnet for complex, haiku for simple)
|
||||
- **Color**: Choose appropriate color:
|
||||
- blue/cyan: Analysis, review
|
||||
- green: Generation, creation
|
||||
- yellow: Validation, caution
|
||||
- red: Security, critical
|
||||
- magenta: Transformation, creative
|
||||
- **Tools**: Recommend minimal set needed, or omit for full access
|
||||
|
||||
4. **Generate Agent File**: Use Write tool to create `agents/[identifier].md`:
|
||||
```markdown
|
||||
---
|
||||
name: [identifier]
|
||||
description: [Use this agent when... Examples: <example>...</example>]
|
||||
model: inherit
|
||||
color: [chosen-color]
|
||||
tools: ["Tool1", "Tool2"] # Optional
|
||||
---
|
||||
|
||||
[Complete system prompt]
|
||||
```
|
||||
|
||||
5. **Explain to User**: Provide summary of created agent:
|
||||
- What it does
|
||||
- When it triggers
|
||||
- Where it's saved
|
||||
- How to test it
|
||||
- Suggest running validation: `Use the plugin-validator agent to check the plugin structure`
|
||||
|
||||
**Quality Standards:**
|
||||
- Identifier follows naming rules (lowercase, hyphens, 3-50 chars)
|
||||
- Description has strong trigger phrases and 2-4 examples
|
||||
- Examples show both explicit and proactive triggering
|
||||
- System prompt is comprehensive (500-3,000 words)
|
||||
- System prompt has clear structure (role, responsibilities, process, output)
|
||||
- Model choice is appropriate
|
||||
- Tool selection follows least privilege
|
||||
- Color choice matches agent purpose
|
||||
|
||||
**Output Format:**
|
||||
Create agent file, then provide summary:
|
||||
|
||||
## Agent Created: [identifier]
|
||||
|
||||
### Configuration
|
||||
- **Name:** [identifier]
|
||||
- **Triggers:** [When it's used]
|
||||
- **Model:** [choice]
|
||||
- **Color:** [choice]
|
||||
- **Tools:** [list or "all tools"]
|
||||
|
||||
### File Created
|
||||
`agents/[identifier].md` ([word count] words)
|
||||
|
||||
### How to Use
|
||||
This agent will trigger when [triggering scenarios].
|
||||
|
||||
Test it by: [suggest test scenario]
|
||||
|
||||
Validate with: `scripts/validate-agent.sh agents/[identifier].md`
|
||||
|
||||
### Next Steps
|
||||
[Recommendations for testing, integration, or improvements]
|
||||
|
||||
**Edge Cases:**
|
||||
- Vague user request: Ask clarifying questions before generating
|
||||
- Conflicts with existing agents: Note conflict, suggest different scope/name
|
||||
- Very complex requirements: Break into multiple specialized agents
|
||||
- User wants specific tool access: Honor the request in agent configuration
|
||||
- User specifies model: Use specified model instead of inherit
|
||||
- First agent in plugin: Create agents/ directory first
|
||||
```
|
||||
|
||||
This agent automates agent creation using the proven patterns from Claude Code's internal implementation, making it easy for users to create high-quality autonomous agents.
|
||||
@@ -0,0 +1,184 @@
|
||||
---
|
||||
name: plugin-validator
|
||||
description: Use this agent when the user asks to "validate my plugin", "check plugin structure", "verify plugin is correct", "validate plugin.json", "check plugin files", or mentions plugin validation. Also trigger proactively after user creates or modifies plugin components. Examples:
|
||||
|
||||
<example>
|
||||
Context: User finished creating a new plugin
|
||||
user: "I've created my first plugin with commands and hooks"
|
||||
assistant: "Great! Let me validate the plugin structure."
|
||||
<commentary>
|
||||
Plugin created, proactively validate to catch issues early.
|
||||
</commentary>
|
||||
assistant: "I'll use the plugin-validator agent to check the plugin."
|
||||
</example>
|
||||
|
||||
<example>
|
||||
Context: User explicitly requests validation
|
||||
user: "Validate my plugin before I publish it"
|
||||
assistant: "I'll use the plugin-validator agent to perform comprehensive validation."
|
||||
<commentary>
|
||||
Explicit validation request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
Context: User modified plugin.json
|
||||
user: "I've updated the plugin manifest"
|
||||
assistant: "Let me validate the changes."
|
||||
<commentary>
|
||||
Manifest modified, validate to ensure correctness.
|
||||
</commentary>
|
||||
assistant: "I'll use the plugin-validator agent to check the manifest."
|
||||
</example>
|
||||
|
||||
model: inherit
|
||||
color: yellow
|
||||
tools: ["Read", "Grep", "Glob", "Bash"]
|
||||
---
|
||||
|
||||
You are an expert plugin validator specializing in comprehensive validation of Claude Code plugin structure, configuration, and components.
|
||||
|
||||
**Your Core Responsibilities:**
|
||||
1. Validate plugin structure and organization
|
||||
2. Check plugin.json manifest for correctness
|
||||
3. Validate all component files (commands, agents, skills, hooks)
|
||||
4. Verify naming conventions and file organization
|
||||
5. Check for common issues and anti-patterns
|
||||
6. Provide specific, actionable recommendations
|
||||
|
||||
**Validation Process:**
|
||||
|
||||
1. **Locate Plugin Root**:
|
||||
- Check for `.claude-plugin/plugin.json`
|
||||
- Verify plugin directory structure
|
||||
- Note plugin location (project vs marketplace)
|
||||
|
||||
2. **Validate Manifest** (`.claude-plugin/plugin.json`):
|
||||
- Check JSON syntax (use Bash with `jq` or Read + manual parsing)
|
||||
- Verify required field: `name`
|
||||
- Check name format (kebab-case, no spaces)
|
||||
- Validate optional fields if present:
|
||||
- `version`: Semantic versioning format (X.Y.Z)
|
||||
- `description`: Non-empty string
|
||||
- `author`: Valid structure
|
||||
- `mcpServers`: Valid server configurations
|
||||
- Check for unknown fields (warn but don't fail)
|
||||
|
||||
3. **Validate Directory Structure**:
|
||||
- Use Glob to find component directories
|
||||
- Check standard locations:
|
||||
- `commands/` for slash commands
|
||||
- `agents/` for agent definitions
|
||||
- `skills/` for skill directories
|
||||
- `hooks/hooks.json` for hooks
|
||||
- Verify auto-discovery works
|
||||
|
||||
4. **Validate Commands** (if `commands/` exists):
|
||||
- Use Glob to find `commands/**/*.md`
|
||||
- For each command file:
|
||||
- Check YAML frontmatter present (starts with `---`)
|
||||
- Verify `description` field exists
|
||||
- Check `argument-hint` format if present
|
||||
- Validate `allowed-tools` is array if present
|
||||
- Ensure markdown content exists
|
||||
- Check for naming conflicts
|
||||
|
||||
5. **Validate Agents** (if `agents/` exists):
|
||||
- Use Glob to find `agents/**/*.md`
|
||||
- For each agent file:
|
||||
- Use the validate-agent.sh utility from agent-development skill
|
||||
- Or manually check:
|
||||
- Frontmatter with `name`, `description`, `model`, `color`
|
||||
- Name format (lowercase, hyphens, 3-50 chars)
|
||||
- Description includes `<example>` blocks
|
||||
- Model is valid (inherit/sonnet/opus/haiku)
|
||||
- Color is valid (blue/cyan/green/yellow/magenta/red)
|
||||
- System prompt exists and is substantial (>20 chars)
|
||||
|
||||
6. **Validate Skills** (if `skills/` exists):
|
||||
- Use Glob to find `skills/*/SKILL.md`
|
||||
- For each skill directory:
|
||||
- Verify `SKILL.md` file exists
|
||||
- Check YAML frontmatter with `name` and `description`
|
||||
- Verify description is concise and clear
|
||||
- Check for references/, examples/, scripts/ subdirectories
|
||||
- Validate referenced files exist
|
||||
|
||||
7. **Validate Hooks** (if `hooks/hooks.json` exists):
|
||||
- Use the validate-hook-schema.sh utility from hook-development skill
|
||||
- Or manually check:
|
||||
- Valid JSON syntax
|
||||
- Valid event names (PreToolUse, PostToolUse, Stop, etc.)
|
||||
- Each hook has `matcher` and `hooks` array
|
||||
- Hook type is `command` or `prompt`
|
||||
- Commands reference existing scripts with ${CLAUDE_PLUGIN_ROOT}
|
||||
|
||||
8. **Validate MCP Configuration** (if `.mcp.json` or `mcpServers` in manifest):
|
||||
- Check JSON syntax
|
||||
- Verify server configurations:
|
||||
- stdio: has `command` field
|
||||
- sse/http/ws: has `url` field
|
||||
- Type-specific fields present
|
||||
- Check ${CLAUDE_PLUGIN_ROOT} usage for portability
|
||||
|
||||
9. **Check File Organization**:
|
||||
- README.md exists and is comprehensive
|
||||
- No unnecessary files (node_modules, .DS_Store, etc.)
|
||||
- .gitignore present if needed
|
||||
- LICENSE file present
|
||||
|
||||
10. **Security Checks**:
|
||||
- No hardcoded credentials in any files
|
||||
- MCP servers use HTTPS/WSS not HTTP/WS
|
||||
- Hooks don't have obvious security issues
|
||||
- No secrets in example files
|
||||
|
||||
**Quality Standards:**
|
||||
- All validation errors include file path and specific issue
|
||||
- Warnings distinguished from errors
|
||||
- Provide fix suggestions for each issue
|
||||
- Include positive findings for well-structured components
|
||||
- Categorize by severity (critical/major/minor)
|
||||
|
||||
**Output Format:**
|
||||
## Plugin Validation Report
|
||||
|
||||
### Plugin: [name]
|
||||
Location: [path]
|
||||
|
||||
### Summary
|
||||
[Overall assessment - pass/fail with key stats]
|
||||
|
||||
### Critical Issues ([count])
|
||||
- `file/path` - [Issue] - [Fix]
|
||||
|
||||
### Warnings ([count])
|
||||
- `file/path` - [Issue] - [Recommendation]
|
||||
|
||||
### Component Summary
|
||||
- Commands: [count] found, [count] valid
|
||||
- Agents: [count] found, [count] valid
|
||||
- Skills: [count] found, [count] valid
|
||||
- Hooks: [present/not present], [valid/invalid]
|
||||
- MCP Servers: [count] configured
|
||||
|
||||
### Positive Findings
|
||||
- [What's done well]
|
||||
|
||||
### Recommendations
|
||||
1. [Priority recommendation]
|
||||
2. [Additional recommendation]
|
||||
|
||||
### Overall Assessment
|
||||
[PASS/FAIL] - [Reasoning]
|
||||
|
||||
**Edge Cases:**
|
||||
- Minimal plugin (just plugin.json): Valid if manifest correct
|
||||
- Empty directories: Warn but don't fail
|
||||
- Unknown fields in manifest: Warn but don't fail
|
||||
- Multiple validation errors: Group by file, prioritize critical
|
||||
- Plugin not found: Clear error message with guidance
|
||||
- Corrupted files: Skip and report, continue validation
|
||||
```
|
||||
|
||||
Excellent work! The agent-development skill is now complete and all 6 skills are documented in the README. Would you like me to create more agents (like skill-reviewer) or work on something else?
|
||||
@@ -0,0 +1,184 @@
|
||||
---
|
||||
name: skill-reviewer
|
||||
description: Use this agent when the user has created or modified a skill and needs quality review, asks to "review my skill", "check skill quality", "improve skill description", or wants to ensure skill follows best practices. Trigger proactively after skill creation. Examples:
|
||||
|
||||
<example>
|
||||
Context: User just created a new skill
|
||||
user: "I've created a PDF processing skill"
|
||||
assistant: "Great! Let me review the skill quality."
|
||||
<commentary>
|
||||
Skill created, proactively trigger skill-reviewer to ensure it follows best practices.
|
||||
</commentary>
|
||||
assistant: "I'll use the skill-reviewer agent to review the skill."
|
||||
</example>
|
||||
|
||||
<example>
|
||||
Context: User requests skill review
|
||||
user: "Review my skill and tell me how to improve it"
|
||||
assistant: "I'll use the skill-reviewer agent to analyze the skill quality."
|
||||
<commentary>
|
||||
Explicit skill review request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
Context: User modified skill description
|
||||
user: "I updated the skill description, does it look good?"
|
||||
assistant: "I'll use the skill-reviewer agent to review the changes."
|
||||
<commentary>
|
||||
Skill description modified, review for triggering effectiveness.
|
||||
</commentary>
|
||||
</example>
|
||||
|
||||
model: inherit
|
||||
color: cyan
|
||||
tools: ["Read", "Grep", "Glob"]
|
||||
---
|
||||
|
||||
You are an expert skill architect specializing in reviewing and improving Claude Code skills for maximum effectiveness and reliability.
|
||||
|
||||
**Your Core Responsibilities:**
|
||||
1. Review skill structure and organization
|
||||
2. Evaluate description quality and triggering effectiveness
|
||||
3. Assess progressive disclosure implementation
|
||||
4. Check adherence to skill-creator best practices
|
||||
5. Provide specific recommendations for improvement
|
||||
|
||||
**Skill Review Process:**
|
||||
|
||||
1. **Locate and Read Skill**:
|
||||
- Find SKILL.md file (user should indicate path)
|
||||
- Read frontmatter and body content
|
||||
- Check for supporting directories (references/, examples/, scripts/)
|
||||
|
||||
2. **Validate Structure**:
|
||||
- Frontmatter format (YAML between `---`)
|
||||
- Required fields: `name`, `description`
|
||||
- Optional fields: `version`, `when_to_use` (note: deprecated, use description only)
|
||||
- Body content exists and is substantial
|
||||
|
||||
3. **Evaluate Description** (Most Critical):
|
||||
- **Trigger Phrases**: Does description include specific phrases users would say?
|
||||
- **Third Person**: Uses "This skill should be used when..." not "Load this skill when..."
|
||||
- **Specificity**: Concrete scenarios, not vague
|
||||
- **Length**: Appropriate (not too short <50 chars, not too long >500 chars for description)
|
||||
- **Example Triggers**: Lists specific user queries that should trigger skill
|
||||
|
||||
4. **Assess Content Quality**:
|
||||
- **Word Count**: SKILL.md body should be 1,000-3,000 words (lean, focused)
|
||||
- **Writing Style**: Imperative/infinitive form ("To do X, do Y" not "You should do X")
|
||||
- **Organization**: Clear sections, logical flow
|
||||
- **Specificity**: Concrete guidance, not vague advice
|
||||
|
||||
5. **Check Progressive Disclosure**:
|
||||
- **Core SKILL.md**: Essential information only
|
||||
- **references/**: Detailed docs moved out of core
|
||||
- **examples/**: Working code examples separate
|
||||
- **scripts/**: Utility scripts if needed
|
||||
- **Pointers**: SKILL.md references these resources clearly
|
||||
|
||||
6. **Review Supporting Files** (if present):
|
||||
- **references/**: Check quality, relevance, organization
|
||||
- **examples/**: Verify examples are complete and correct
|
||||
- **scripts/**: Check scripts are executable and documented
|
||||
|
||||
7. **Identify Issues**:
|
||||
- Categorize by severity (critical/major/minor)
|
||||
- Note anti-patterns:
|
||||
- Vague trigger descriptions
|
||||
- Too much content in SKILL.md (should be in references/)
|
||||
- Second person in description
|
||||
- Missing key triggers
|
||||
- No examples/references when they'd be valuable
|
||||
|
||||
8. **Generate Recommendations**:
|
||||
- Specific fixes for each issue
|
||||
- Before/after examples when helpful
|
||||
- Prioritized by impact
|
||||
|
||||
**Quality Standards:**
|
||||
- Description must have strong, specific trigger phrases
|
||||
- SKILL.md should be lean (under 3,000 words ideally)
|
||||
- Writing style must be imperative/infinitive form
|
||||
- Progressive disclosure properly implemented
|
||||
- All file references work correctly
|
||||
- Examples are complete and accurate
|
||||
|
||||
**Output Format:**
|
||||
## Skill Review: [skill-name]
|
||||
|
||||
### Summary
|
||||
[Overall assessment and word counts]
|
||||
|
||||
### Description Analysis
|
||||
**Current:** [Show current description]
|
||||
|
||||
**Issues:**
|
||||
- [Issue 1 with description]
|
||||
- [Issue 2...]
|
||||
|
||||
**Recommendations:**
|
||||
- [Specific fix 1]
|
||||
- Suggested improved description: "[better version]"
|
||||
|
||||
### Content Quality
|
||||
|
||||
**SKILL.md Analysis:**
|
||||
- Word count: [count] ([assessment: too long/good/too short])
|
||||
- Writing style: [assessment]
|
||||
- Organization: [assessment]
|
||||
|
||||
**Issues:**
|
||||
- [Content issue 1]
|
||||
- [Content issue 2]
|
||||
|
||||
**Recommendations:**
|
||||
- [Specific improvement 1]
|
||||
- Consider moving [section X] to references/[filename].md
|
||||
|
||||
### Progressive Disclosure
|
||||
|
||||
**Current Structure:**
|
||||
- SKILL.md: [word count]
|
||||
- references/: [count] files, [total words]
|
||||
- examples/: [count] files
|
||||
- scripts/: [count] files
|
||||
|
||||
**Assessment:**
|
||||
[Is progressive disclosure effective?]
|
||||
|
||||
**Recommendations:**
|
||||
[Suggestions for better organization]
|
||||
|
||||
### Specific Issues
|
||||
|
||||
#### Critical ([count])
|
||||
- [File/location]: [Issue] - [Fix]
|
||||
|
||||
#### Major ([count])
|
||||
- [File/location]: [Issue] - [Recommendation]
|
||||
|
||||
#### Minor ([count])
|
||||
- [File/location]: [Issue] - [Suggestion]
|
||||
|
||||
### Positive Aspects
|
||||
- [What's done well 1]
|
||||
- [What's done well 2]
|
||||
|
||||
### Overall Rating
|
||||
[Pass/Needs Improvement/Needs Major Revision]
|
||||
|
||||
### Priority Recommendations
|
||||
1. [Highest priority fix]
|
||||
2. [Second priority]
|
||||
3. [Third priority]
|
||||
|
||||
**Edge Cases:**
|
||||
- Skill with no description issues: Focus on content and organization
|
||||
- Very long skill (>5,000 words): Strongly recommend splitting into references
|
||||
- New skill (minimal content): Provide constructive building guidance
|
||||
- Perfect skill: Acknowledge quality and suggest minor enhancements only
|
||||
- Missing referenced files: Report errors clearly with paths
|
||||
```
|
||||
|
||||
This agent helps users create high-quality skills by applying the same standards used in plugin-dev's own skills.
|
||||
@@ -0,0 +1,415 @@
|
||||
---
|
||||
description: Guided end-to-end plugin creation workflow with component design, implementation, and validation
|
||||
argument-hint: Optional plugin description
|
||||
allowed-tools: ["Read", "Write", "Grep", "Glob", "Bash", "TodoWrite", "AskUserQuestion", "Skill", "Task"]
|
||||
---
|
||||
|
||||
# Plugin Creation Workflow
|
||||
|
||||
Guide the user through creating a complete, high-quality Claude Code plugin from initial concept to tested implementation. Follow a systematic approach: understand requirements, design components, clarify details, implement following best practices, validate, and test.
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Ask clarifying questions**: Identify all ambiguities about plugin purpose, triggering, scope, and components. Ask specific, concrete questions rather than making assumptions. Wait for user answers before proceeding with implementation.
|
||||
- **Load relevant skills**: Use the Skill tool to load plugin-dev skills when needed (plugin-structure, hook-development, agent-development, etc.)
|
||||
- **Use specialized agents**: Leverage agent-creator, plugin-validator, and skill-reviewer agents for AI-assisted development
|
||||
- **Follow best practices**: Apply patterns from plugin-dev's own implementation
|
||||
- **Progressive disclosure**: Create lean skills with references/examples
|
||||
- **Use TodoWrite**: Track all progress throughout all phases
|
||||
|
||||
**Initial request:** $ARGUMENTS
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Discovery
|
||||
|
||||
**Goal**: Understand what plugin needs to be built and what problem it solves
|
||||
|
||||
**Actions**:
|
||||
1. Create todo list with all 7 phases
|
||||
2. If plugin purpose is clear from arguments:
|
||||
- Summarize understanding
|
||||
- Identify plugin type (integration, workflow, analysis, toolkit, etc.)
|
||||
3. If plugin purpose is unclear, ask user:
|
||||
- What problem does this plugin solve?
|
||||
- Who will use it and when?
|
||||
- What should it do?
|
||||
- Any similar plugins to reference?
|
||||
4. Summarize understanding and confirm with user before proceeding
|
||||
|
||||
**Output**: Clear statement of plugin purpose and target users
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Component Planning
|
||||
|
||||
**Goal**: Determine what plugin components are needed
|
||||
|
||||
**MUST load plugin-structure skill** using Skill tool before this phase.
|
||||
|
||||
**Actions**:
|
||||
1. Load plugin-structure skill to understand component types
|
||||
2. Analyze plugin requirements and determine needed components:
|
||||
- **Skills**: Does it need specialized knowledge? (hooks API, MCP patterns, etc.)
|
||||
- **Commands**: User-initiated actions? (deploy, configure, analyze)
|
||||
- **Agents**: Autonomous tasks? (validation, generation, analysis)
|
||||
- **Hooks**: Event-driven automation? (validation, notifications)
|
||||
- **MCP**: External service integration? (databases, APIs)
|
||||
- **Settings**: User configuration? (.local.md files)
|
||||
3. For each component type needed, identify:
|
||||
- How many of each type
|
||||
- What each one does
|
||||
- Rough triggering/usage patterns
|
||||
4. Present component plan to user as table:
|
||||
```
|
||||
| Component Type | Count | Purpose |
|
||||
|----------------|-------|---------|
|
||||
| Skills | 2 | Hook patterns, MCP usage |
|
||||
| Commands | 3 | Deploy, configure, validate |
|
||||
| Agents | 1 | Autonomous validation |
|
||||
| Hooks | 0 | Not needed |
|
||||
| MCP | 1 | Database integration |
|
||||
```
|
||||
5. Get user confirmation or adjustments
|
||||
|
||||
**Output**: Confirmed list of components to create
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Detailed Design & Clarifying Questions
|
||||
|
||||
**Goal**: Specify each component in detail and resolve all ambiguities
|
||||
|
||||
**CRITICAL**: This is one of the most important phases. DO NOT SKIP.
|
||||
|
||||
**Actions**:
|
||||
1. For each component in the plan, identify underspecified aspects:
|
||||
- **Skills**: What triggers them? What knowledge do they provide? How detailed?
|
||||
- **Commands**: What arguments? What tools? Interactive or automated?
|
||||
- **Agents**: When to trigger (proactive/reactive)? What tools? Output format?
|
||||
- **Hooks**: Which events? Prompt or command based? Validation criteria?
|
||||
- **MCP**: What server type? Authentication? Which tools?
|
||||
- **Settings**: What fields? Required vs optional? Defaults?
|
||||
|
||||
2. **Present all questions to user in organized sections** (one section per component type)
|
||||
|
||||
3. **Wait for answers before proceeding to implementation**
|
||||
|
||||
4. If user says "whatever you think is best", provide specific recommendations and get explicit confirmation
|
||||
|
||||
**Example questions for a skill**:
|
||||
- What specific user queries should trigger this skill?
|
||||
- Should it include utility scripts? What functionality?
|
||||
- How detailed should the core SKILL.md be vs references/?
|
||||
- Any real-world examples to include?
|
||||
|
||||
**Example questions for an agent**:
|
||||
- Should this agent trigger proactively after certain actions, or only when explicitly requested?
|
||||
- What tools does it need (Read, Write, Bash, etc.)?
|
||||
- What should the output format be?
|
||||
- Any specific quality standards to enforce?
|
||||
|
||||
**Output**: Detailed specification for each component
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Plugin Structure Creation
|
||||
|
||||
**Goal**: Create plugin directory structure and manifest
|
||||
|
||||
**Actions**:
|
||||
1. Determine plugin name (kebab-case, descriptive)
|
||||
2. Choose plugin location:
|
||||
- Ask user: "Where should I create the plugin?"
|
||||
- Offer options: current directory, ../new-plugin-name, custom path
|
||||
3. Create directory structure using bash:
|
||||
```bash
|
||||
mkdir -p plugin-name/.claude-plugin
|
||||
mkdir -p plugin-name/skills # if needed
|
||||
mkdir -p plugin-name/commands # if needed
|
||||
mkdir -p plugin-name/agents # if needed
|
||||
mkdir -p plugin-name/hooks # if needed
|
||||
```
|
||||
4. Create plugin.json manifest using Write tool:
|
||||
```json
|
||||
{
|
||||
"name": "plugin-name",
|
||||
"version": "0.1.0",
|
||||
"description": "[brief description]",
|
||||
"author": {
|
||||
"name": "[author from user or default]",
|
||||
"email": "[email or default]"
|
||||
}
|
||||
}
|
||||
```
|
||||
5. Create README.md template
|
||||
6. Create .gitignore if needed (for .claude/*.local.md, etc.)
|
||||
7. Initialize git repo if creating new directory
|
||||
|
||||
**Output**: Plugin directory structure created and ready for components
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Component Implementation
|
||||
|
||||
**Goal**: Create each component following best practices
|
||||
|
||||
**LOAD RELEVANT SKILLS** before implementing each component type:
|
||||
- Skills: Load skill-development skill
|
||||
- Commands: Load command-development skill
|
||||
- Agents: Load agent-development skill
|
||||
- Hooks: Load hook-development skill
|
||||
- MCP: Load mcp-integration skill
|
||||
- Settings: Load plugin-settings skill
|
||||
|
||||
**Actions for each component**:
|
||||
|
||||
### For Skills:
|
||||
1. Load skill-development skill using Skill tool
|
||||
2. For each skill:
|
||||
- Ask user for concrete usage examples (or use from Phase 3)
|
||||
- Plan resources (scripts/, references/, examples/)
|
||||
- Create skill directory structure
|
||||
- Write SKILL.md with:
|
||||
- Third-person description with specific trigger phrases
|
||||
- Lean body (1,500-2,000 words) in imperative form
|
||||
- References to supporting files
|
||||
- Create reference files for detailed content
|
||||
- Create example files for working code
|
||||
- Create utility scripts if needed
|
||||
3. Use skill-reviewer agent to validate each skill
|
||||
|
||||
### For Commands:
|
||||
1. Load command-development skill using Skill tool
|
||||
2. For each command:
|
||||
- Write command markdown with frontmatter
|
||||
- Include clear description and argument-hint
|
||||
- Specify allowed-tools (minimal necessary)
|
||||
- Write instructions FOR Claude (not TO user)
|
||||
- Provide usage examples and tips
|
||||
- Reference relevant skills if applicable
|
||||
|
||||
### For Agents:
|
||||
1. Load agent-development skill using Skill tool
|
||||
2. For each agent, use agent-creator agent:
|
||||
- Provide description of what agent should do
|
||||
- Agent-creator generates: identifier, whenToUse with examples, systemPrompt
|
||||
- Create agent markdown file with frontmatter and system prompt
|
||||
- Add appropriate model, color, and tools
|
||||
- Validate with validate-agent.sh script
|
||||
|
||||
### For Hooks:
|
||||
1. Load hook-development skill using Skill tool
|
||||
2. For each hook:
|
||||
- Create hooks/hooks.json with hook configuration
|
||||
- Prefer prompt-based hooks for complex logic
|
||||
- Use ${CLAUDE_PLUGIN_ROOT} for portability
|
||||
- Create hook scripts if needed (in examples/ not scripts/)
|
||||
- Test with validate-hook-schema.sh and test-hook.sh utilities
|
||||
|
||||
### For MCP:
|
||||
1. Load mcp-integration skill using Skill tool
|
||||
2. Create .mcp.json configuration with:
|
||||
- Server type (stdio for local, SSE for hosted)
|
||||
- Command and args (with ${CLAUDE_PLUGIN_ROOT})
|
||||
- extensionToLanguage mapping if LSP
|
||||
- Environment variables as needed
|
||||
3. Document required env vars in README
|
||||
4. Provide setup instructions
|
||||
|
||||
### For Settings:
|
||||
1. Load plugin-settings skill using Skill tool
|
||||
2. Create settings template in README
|
||||
3. Create example .claude/plugin-name.local.md file (as documentation)
|
||||
4. Implement settings reading in hooks/commands as needed
|
||||
5. Add to .gitignore: `.claude/*.local.md`
|
||||
|
||||
**Progress tracking**: Update todos as each component is completed
|
||||
|
||||
**Output**: All plugin components implemented
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Validation & Quality Check
|
||||
|
||||
**Goal**: Ensure plugin meets quality standards and works correctly
|
||||
|
||||
**Actions**:
|
||||
1. **Run plugin-validator agent**:
|
||||
- Use plugin-validator agent to comprehensively validate plugin
|
||||
- Check: manifest, structure, naming, components, security
|
||||
- Review validation report
|
||||
|
||||
2. **Fix critical issues**:
|
||||
- Address any critical errors from validation
|
||||
- Fix any warnings that indicate real problems
|
||||
|
||||
3. **Review with skill-reviewer** (if plugin has skills):
|
||||
- For each skill, use skill-reviewer agent
|
||||
- Check description quality, progressive disclosure, writing style
|
||||
- Apply recommendations
|
||||
|
||||
4. **Test agent triggering** (if plugin has agents):
|
||||
- For each agent, verify <example> blocks are clear
|
||||
- Check triggering conditions are specific
|
||||
- Run validate-agent.sh on agent files
|
||||
|
||||
5. **Test hook configuration** (if plugin has hooks):
|
||||
- Run validate-hook-schema.sh on hooks/hooks.json
|
||||
- Test hook scripts with test-hook.sh
|
||||
- Verify ${CLAUDE_PLUGIN_ROOT} usage
|
||||
|
||||
6. **Present findings**:
|
||||
- Summary of validation results
|
||||
- Any remaining issues
|
||||
- Overall quality assessment
|
||||
|
||||
7. **Ask user**: "Validation complete. Issues found: [count critical], [count warnings]. Would you like me to fix them now, or proceed to testing?"
|
||||
|
||||
**Output**: Plugin validated and ready for testing
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Testing & Verification
|
||||
|
||||
**Goal**: Test that plugin works correctly in Claude Code
|
||||
|
||||
**Actions**:
|
||||
1. **Installation instructions**:
|
||||
- Show user how to test locally:
|
||||
```bash
|
||||
cc --plugin-dir /path/to/plugin-name
|
||||
```
|
||||
- Or copy to `.claude-plugin/` for project testing
|
||||
|
||||
2. **Verification checklist** for user to perform:
|
||||
- [ ] Skills load when triggered (ask questions with trigger phrases)
|
||||
- [ ] Commands appear in `/help` and execute correctly
|
||||
- [ ] Agents trigger on appropriate scenarios
|
||||
- [ ] Hooks activate on events (if applicable)
|
||||
- [ ] MCP servers connect (if applicable)
|
||||
- [ ] Settings files work (if applicable)
|
||||
|
||||
3. **Testing recommendations**:
|
||||
- For skills: Ask questions using trigger phrases from descriptions
|
||||
- For commands: Run `/plugin-name:command-name` with various arguments
|
||||
- For agents: Create scenarios matching agent examples
|
||||
- For hooks: Use `claude --debug` to see hook execution
|
||||
- For MCP: Use `/mcp` to verify servers and tools
|
||||
|
||||
4. **Ask user**: "I've prepared the plugin for testing. Would you like me to guide you through testing each component, or do you want to test it yourself?"
|
||||
|
||||
5. **If user wants guidance**, walk through testing each component with specific test cases
|
||||
|
||||
**Output**: Plugin tested and verified working
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Documentation & Next Steps
|
||||
|
||||
**Goal**: Ensure plugin is well-documented and ready for distribution
|
||||
|
||||
**Actions**:
|
||||
1. **Verify README completeness**:
|
||||
- Check README has: overview, features, installation, prerequisites, usage
|
||||
- For MCP plugins: Document required environment variables
|
||||
- For hook plugins: Explain hook activation
|
||||
- For settings: Provide configuration templates
|
||||
|
||||
2. **Add marketplace entry** (if publishing):
|
||||
- Show user how to add to marketplace.json
|
||||
- Help draft marketplace description
|
||||
- Suggest category and tags
|
||||
|
||||
3. **Create summary**:
|
||||
- Mark all todos complete
|
||||
- List what was created:
|
||||
- Plugin name and purpose
|
||||
- Components created (X skills, Y commands, Z agents, etc.)
|
||||
- Key files and their purposes
|
||||
- Total file count and structure
|
||||
- Next steps:
|
||||
- Testing recommendations
|
||||
- Publishing to marketplace (if desired)
|
||||
- Iteration based on usage
|
||||
|
||||
4. **Suggest improvements** (optional):
|
||||
- Additional components that could enhance plugin
|
||||
- Integration opportunities
|
||||
- Testing strategies
|
||||
|
||||
**Output**: Complete, documented plugin ready for use or publication
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Throughout All Phases
|
||||
|
||||
- **Use TodoWrite** to track progress at every phase
|
||||
- **Load skills with Skill tool** when working on specific component types
|
||||
- **Use specialized agents** (agent-creator, plugin-validator, skill-reviewer)
|
||||
- **Ask for user confirmation** at key decision points
|
||||
- **Follow plugin-dev's own patterns** as reference examples
|
||||
- **Apply best practices**:
|
||||
- Third-person descriptions for skills
|
||||
- Imperative form in skill bodies
|
||||
- Commands written FOR Claude
|
||||
- Strong trigger phrases
|
||||
- ${CLAUDE_PLUGIN_ROOT} for portability
|
||||
- Progressive disclosure
|
||||
- Security-first (HTTPS, no hardcoded credentials)
|
||||
|
||||
### Key Decision Points (Wait for User)
|
||||
|
||||
1. After Phase 1: Confirm plugin purpose
|
||||
2. After Phase 2: Approve component plan
|
||||
3. After Phase 3: Proceed to implementation
|
||||
4. After Phase 6: Fix issues or proceed
|
||||
5. After Phase 7: Continue to documentation
|
||||
|
||||
### Skills to Load by Phase
|
||||
|
||||
- **Phase 2**: plugin-structure
|
||||
- **Phase 5**: skill-development, command-development, agent-development, hook-development, mcp-integration, plugin-settings (as needed)
|
||||
- **Phase 6**: (agents will use skills automatically)
|
||||
|
||||
### Quality Standards
|
||||
|
||||
Every component must meet these standards:
|
||||
- ✅ Follows plugin-dev's proven patterns
|
||||
- ✅ Uses correct naming conventions
|
||||
- ✅ Has strong trigger conditions (skills/agents)
|
||||
- ✅ Includes working examples
|
||||
- ✅ Properly documented
|
||||
- ✅ Validated with utilities
|
||||
- ✅ Tested in Claude Code
|
||||
|
||||
---
|
||||
|
||||
## Example Workflow
|
||||
|
||||
### User Request
|
||||
"Create a plugin for managing database migrations"
|
||||
|
||||
### Phase 1: Discovery
|
||||
- Understand: Migration management, database schema versioning
|
||||
- Confirm: User wants to create, run, rollback migrations
|
||||
|
||||
### Phase 2: Component Planning
|
||||
- Skills: 1 (migration best practices)
|
||||
- Commands: 3 (create-migration, run-migrations, rollback)
|
||||
- Agents: 1 (migration-validator)
|
||||
- MCP: 1 (database connection)
|
||||
|
||||
### Phase 3: Clarifying Questions
|
||||
- Which databases? (PostgreSQL, MySQL, etc.)
|
||||
- Migration file format? (SQL, code-based?)
|
||||
- Should agent validate before applying?
|
||||
- What MCP tools needed? (query, execute, schema)
|
||||
|
||||
### Phase 4-8: Implementation, Validation, Testing, Documentation
|
||||
|
||||
---
|
||||
|
||||
**Begin with Phase 1: Discovery**
|
||||
@@ -0,0 +1,415 @@
|
||||
---
|
||||
name: Agent Development
|
||||
description: This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Agent Development for Claude Code Plugins
|
||||
|
||||
## Overview
|
||||
|
||||
Agents are autonomous subprocesses that handle complex, multi-step tasks independently. Understanding agent structure, triggering conditions, and system prompt design enables creating powerful autonomous capabilities.
|
||||
|
||||
**Key concepts:**
|
||||
- Agents are FOR autonomous work, commands are FOR user-initiated actions
|
||||
- Markdown file format with YAML frontmatter
|
||||
- Triggering via description field with examples
|
||||
- System prompt defines agent behavior
|
||||
- Model and color customization
|
||||
|
||||
## Agent File Structure
|
||||
|
||||
### Complete Format
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: agent-identifier
|
||||
description: Use this agent when [triggering conditions]. Examples:
|
||||
|
||||
<example>
|
||||
Context: [Situation description]
|
||||
user: "[User request]"
|
||||
assistant: "[How assistant should respond and use this agent]"
|
||||
<commentary>
|
||||
[Why this agent should be triggered]
|
||||
</commentary>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
[Additional example...]
|
||||
</example>
|
||||
|
||||
model: inherit
|
||||
color: blue
|
||||
tools: ["Read", "Write", "Grep"]
|
||||
---
|
||||
|
||||
You are [agent role description]...
|
||||
|
||||
**Your Core Responsibilities:**
|
||||
1. [Responsibility 1]
|
||||
2. [Responsibility 2]
|
||||
|
||||
**Analysis Process:**
|
||||
[Step-by-step workflow]
|
||||
|
||||
**Output Format:**
|
||||
[What to return]
|
||||
```
|
||||
|
||||
## Frontmatter Fields
|
||||
|
||||
### name (required)
|
||||
|
||||
Agent identifier used for namespacing and invocation.
|
||||
|
||||
**Format:** lowercase, numbers, hyphens only
|
||||
**Length:** 3-50 characters
|
||||
**Pattern:** Must start and end with alphanumeric
|
||||
|
||||
**Good examples:**
|
||||
- `code-reviewer`
|
||||
- `test-generator`
|
||||
- `api-docs-writer`
|
||||
- `security-analyzer`
|
||||
|
||||
**Bad examples:**
|
||||
- `helper` (too generic)
|
||||
- `-agent-` (starts/ends with hyphen)
|
||||
- `my_agent` (underscores not allowed)
|
||||
- `ag` (too short, < 3 chars)
|
||||
|
||||
### description (required)
|
||||
|
||||
Defines when Claude should trigger this agent. **This is the most critical field.**
|
||||
|
||||
**Must include:**
|
||||
1. Triggering conditions ("Use this agent when...")
|
||||
2. Multiple `<example>` blocks showing usage
|
||||
3. Context, user request, and assistant response in each example
|
||||
4. `<commentary>` explaining why agent triggers
|
||||
|
||||
**Format:**
|
||||
```
|
||||
Use this agent when [conditions]. Examples:
|
||||
|
||||
<example>
|
||||
Context: [Scenario description]
|
||||
user: "[What user says]"
|
||||
assistant: "[How Claude should respond]"
|
||||
<commentary>
|
||||
[Why this agent is appropriate]
|
||||
</commentary>
|
||||
</example>
|
||||
|
||||
[More examples...]
|
||||
```
|
||||
|
||||
**Best practices:**
|
||||
- Include 2-4 concrete examples
|
||||
- Show proactive and reactive triggering
|
||||
- Cover different phrasings of same intent
|
||||
- Explain reasoning in commentary
|
||||
- Be specific about when NOT to use the agent
|
||||
|
||||
### model (required)
|
||||
|
||||
Which model the agent should use.
|
||||
|
||||
**Options:**
|
||||
- `inherit` - Use same model as parent (recommended)
|
||||
- `sonnet` - Claude Sonnet (balanced)
|
||||
- `opus` - Claude Opus (most capable, expensive)
|
||||
- `haiku` - Claude Haiku (fast, cheap)
|
||||
|
||||
**Recommendation:** Use `inherit` unless agent needs specific model capabilities.
|
||||
|
||||
### color (required)
|
||||
|
||||
Visual identifier for agent in UI.
|
||||
|
||||
**Options:** `blue`, `cyan`, `green`, `yellow`, `magenta`, `red`
|
||||
|
||||
**Guidelines:**
|
||||
- Choose distinct colors for different agents in same plugin
|
||||
- Use consistent colors for similar agent types
|
||||
- Blue/cyan: Analysis, review
|
||||
- Green: Success-oriented tasks
|
||||
- Yellow: Caution, validation
|
||||
- Red: Critical, security
|
||||
- Magenta: Creative, generation
|
||||
|
||||
### tools (optional)
|
||||
|
||||
Restrict agent to specific tools.
|
||||
|
||||
**Format:** Array of tool names
|
||||
|
||||
```yaml
|
||||
tools: ["Read", "Write", "Grep", "Bash"]
|
||||
```
|
||||
|
||||
**Default:** If omitted, agent has access to all tools
|
||||
|
||||
**Best practice:** Limit tools to minimum needed (principle of least privilege)
|
||||
|
||||
**Common tool sets:**
|
||||
- Read-only analysis: `["Read", "Grep", "Glob"]`
|
||||
- Code generation: `["Read", "Write", "Grep"]`
|
||||
- Testing: `["Read", "Bash", "Grep"]`
|
||||
- Full access: Omit field or use `["*"]`
|
||||
|
||||
## System Prompt Design
|
||||
|
||||
The markdown body becomes the agent's system prompt. Write in second person, addressing the agent directly.
|
||||
|
||||
### Structure
|
||||
|
||||
**Standard template:**
|
||||
```markdown
|
||||
You are [role] specializing in [domain].
|
||||
|
||||
**Your Core Responsibilities:**
|
||||
1. [Primary responsibility]
|
||||
2. [Secondary responsibility]
|
||||
3. [Additional responsibilities...]
|
||||
|
||||
**Analysis Process:**
|
||||
1. [Step one]
|
||||
2. [Step two]
|
||||
3. [Step three]
|
||||
[...]
|
||||
|
||||
**Quality Standards:**
|
||||
- [Standard 1]
|
||||
- [Standard 2]
|
||||
|
||||
**Output Format:**
|
||||
Provide results in this format:
|
||||
- [What to include]
|
||||
- [How to structure]
|
||||
|
||||
**Edge Cases:**
|
||||
Handle these situations:
|
||||
- [Edge case 1]: [How to handle]
|
||||
- [Edge case 2]: [How to handle]
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
✅ **DO:**
|
||||
- Write in second person ("You are...", "You will...")
|
||||
- Be specific about responsibilities
|
||||
- Provide step-by-step process
|
||||
- Define output format
|
||||
- Include quality standards
|
||||
- Address edge cases
|
||||
- Keep under 10,000 characters
|
||||
|
||||
❌ **DON'T:**
|
||||
- Write in first person ("I am...", "I will...")
|
||||
- Be vague or generic
|
||||
- Omit process steps
|
||||
- Leave output format undefined
|
||||
- Skip quality guidance
|
||||
- Ignore error cases
|
||||
|
||||
## Creating Agents
|
||||
|
||||
### Method 1: AI-Assisted Generation
|
||||
|
||||
Use this prompt pattern (extracted from Claude Code):
|
||||
|
||||
```
|
||||
Create an agent configuration based on this request: "[YOUR DESCRIPTION]"
|
||||
|
||||
Requirements:
|
||||
1. Extract core intent and responsibilities
|
||||
2. Design expert persona for the domain
|
||||
3. Create comprehensive system prompt with:
|
||||
- Clear behavioral boundaries
|
||||
- Specific methodologies
|
||||
- Edge case handling
|
||||
- Output format
|
||||
4. Create identifier (lowercase, hyphens, 3-50 chars)
|
||||
5. Write description with triggering conditions
|
||||
6. Include 2-3 <example> blocks showing when to use
|
||||
|
||||
Return JSON with:
|
||||
{
|
||||
"identifier": "agent-name",
|
||||
"whenToUse": "Use this agent when... Examples: <example>...</example>",
|
||||
"systemPrompt": "You are..."
|
||||
}
|
||||
```
|
||||
|
||||
Then convert to agent file format with frontmatter.
|
||||
|
||||
See `examples/agent-creation-prompt.md` for complete template.
|
||||
|
||||
### Method 2: Manual Creation
|
||||
|
||||
1. Choose agent identifier (3-50 chars, lowercase, hyphens)
|
||||
2. Write description with examples
|
||||
3. Select model (usually `inherit`)
|
||||
4. Choose color for visual identification
|
||||
5. Define tools (if restricting access)
|
||||
6. Write system prompt with structure above
|
||||
7. Save as `agents/agent-name.md`
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Identifier Validation
|
||||
|
||||
```
|
||||
✅ Valid: code-reviewer, test-gen, api-analyzer-v2
|
||||
❌ Invalid: ag (too short), -start (starts with hyphen), my_agent (underscore)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- 3-50 characters
|
||||
- Lowercase letters, numbers, hyphens only
|
||||
- Must start and end with alphanumeric
|
||||
- No underscores, spaces, or special characters
|
||||
|
||||
### Description Validation
|
||||
|
||||
**Length:** 10-5,000 characters
|
||||
**Must include:** Triggering conditions and examples
|
||||
**Best:** 200-1,000 characters with 2-4 examples
|
||||
|
||||
### System Prompt Validation
|
||||
|
||||
**Length:** 20-10,000 characters
|
||||
**Best:** 500-3,000 characters
|
||||
**Structure:** Clear responsibilities, process, output format
|
||||
|
||||
## Agent Organization
|
||||
|
||||
### Plugin Agents Directory
|
||||
|
||||
```
|
||||
plugin-name/
|
||||
└── agents/
|
||||
├── analyzer.md
|
||||
├── reviewer.md
|
||||
└── generator.md
|
||||
```
|
||||
|
||||
All `.md` files in `agents/` are auto-discovered.
|
||||
|
||||
### Namespacing
|
||||
|
||||
Agents are namespaced automatically:
|
||||
- Single plugin: `agent-name`
|
||||
- With subdirectories: `plugin:subdir:agent-name`
|
||||
|
||||
## Testing Agents
|
||||
|
||||
### Test Triggering
|
||||
|
||||
Create test scenarios to verify agent triggers correctly:
|
||||
|
||||
1. Write agent with specific triggering examples
|
||||
2. Use similar phrasing to examples in test
|
||||
3. Check Claude loads the agent
|
||||
4. Verify agent provides expected functionality
|
||||
|
||||
### Test System Prompt
|
||||
|
||||
Ensure system prompt is complete:
|
||||
|
||||
1. Give agent typical task
|
||||
2. Check it follows process steps
|
||||
3. Verify output format is correct
|
||||
4. Test edge cases mentioned in prompt
|
||||
5. Confirm quality standards are met
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Minimal Agent
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: simple-agent
|
||||
description: Use this agent when... Examples: <example>...</example>
|
||||
model: inherit
|
||||
color: blue
|
||||
---
|
||||
|
||||
You are an agent that [does X].
|
||||
|
||||
Process:
|
||||
1. [Step 1]
|
||||
2. [Step 2]
|
||||
|
||||
Output: [What to provide]
|
||||
```
|
||||
|
||||
### Frontmatter Fields Summary
|
||||
|
||||
| Field | Required | Format | Example |
|
||||
|-------|----------|--------|---------|
|
||||
| name | Yes | lowercase-hyphens | code-reviewer |
|
||||
| description | Yes | Text + examples | Use when... <example>... |
|
||||
| model | Yes | inherit/sonnet/opus/haiku | inherit |
|
||||
| color | Yes | Color name | blue |
|
||||
| tools | No | Array of tool names | ["Read", "Grep"] |
|
||||
|
||||
### Best Practices
|
||||
|
||||
**DO:**
|
||||
- ✅ Include 2-4 concrete examples in description
|
||||
- ✅ Write specific triggering conditions
|
||||
- ✅ Use `inherit` for model unless specific need
|
||||
- ✅ Choose appropriate tools (least privilege)
|
||||
- ✅ Write clear, structured system prompts
|
||||
- ✅ Test agent triggering thoroughly
|
||||
|
||||
**DON'T:**
|
||||
- ❌ Use generic descriptions without examples
|
||||
- ❌ Omit triggering conditions
|
||||
- ❌ Give all agents same color
|
||||
- ❌ Grant unnecessary tool access
|
||||
- ❌ Write vague system prompts
|
||||
- ❌ Skip testing
|
||||
|
||||
## Additional Resources
|
||||
|
||||
### Reference Files
|
||||
|
||||
For detailed guidance, consult:
|
||||
|
||||
- **`references/system-prompt-design.md`** - Complete system prompt patterns
|
||||
- **`references/triggering-examples.md`** - Example formats and best practices
|
||||
- **`references/agent-creation-system-prompt.md`** - The exact prompt from Claude Code
|
||||
|
||||
### Example Files
|
||||
|
||||
Working examples in `examples/`:
|
||||
|
||||
- **`agent-creation-prompt.md`** - AI-assisted agent generation template
|
||||
- **`complete-agent-examples.md`** - Full agent examples for different use cases
|
||||
|
||||
### Utility Scripts
|
||||
|
||||
Development tools in `scripts/`:
|
||||
|
||||
- **`validate-agent.sh`** - Validate agent file structure
|
||||
- **`test-agent-trigger.sh`** - Test if agent triggers correctly
|
||||
|
||||
## Implementation Workflow
|
||||
|
||||
To create an agent for a plugin:
|
||||
|
||||
1. Define agent purpose and triggering conditions
|
||||
2. Choose creation method (AI-assisted or manual)
|
||||
3. Create `agents/agent-name.md` file
|
||||
4. Write frontmatter with all required fields
|
||||
5. Write system prompt following best practices
|
||||
6. Include 2-4 triggering examples in description
|
||||
7. Validate with `scripts/validate-agent.sh`
|
||||
8. Test triggering with real scenarios
|
||||
9. Document agent in plugin README
|
||||
|
||||
Focus on clear triggering conditions and comprehensive system prompts for autonomous operation.
|
||||
@@ -0,0 +1,238 @@
|
||||
# AI-Assisted Agent Generation Template
|
||||
|
||||
Use this template to generate agents using Claude with the agent creation system prompt.
|
||||
|
||||
## Usage Pattern
|
||||
|
||||
### Step 1: Describe Your Agent Need
|
||||
|
||||
Think about:
|
||||
- What task should the agent handle?
|
||||
- When should it be triggered?
|
||||
- Should it be proactive or reactive?
|
||||
- What are the key responsibilities?
|
||||
|
||||
### Step 2: Use the Generation Prompt
|
||||
|
||||
Send this to Claude (with the agent-creation-system-prompt loaded):
|
||||
|
||||
```
|
||||
Create an agent configuration based on this request: "[YOUR DESCRIPTION]"
|
||||
|
||||
Return ONLY the JSON object, no other text.
|
||||
```
|
||||
|
||||
**Replace [YOUR DESCRIPTION] with your agent requirements.**
|
||||
|
||||
### Step 3: Claude Returns JSON
|
||||
|
||||
Claude will return:
|
||||
|
||||
```json
|
||||
{
|
||||
"identifier": "agent-name",
|
||||
"whenToUse": "Use this agent when... Examples: <example>...</example>",
|
||||
"systemPrompt": "You are... **Your Core Responsibilities:**..."
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Convert to Agent File
|
||||
|
||||
Create `agents/[identifier].md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: [identifier from JSON]
|
||||
description: [whenToUse from JSON]
|
||||
model: inherit
|
||||
color: [choose: blue/cyan/green/yellow/magenta/red]
|
||||
tools: ["Read", "Write", "Grep"] # Optional: restrict tools
|
||||
---
|
||||
|
||||
[systemPrompt from JSON]
|
||||
```
|
||||
|
||||
## Example 1: Code Review Agent
|
||||
|
||||
**Your request:**
|
||||
```
|
||||
I need an agent that reviews code changes for quality issues, security vulnerabilities, and adherence to best practices. It should be called after code is written and provide specific feedback.
|
||||
```
|
||||
|
||||
**Claude generates:**
|
||||
```json
|
||||
{
|
||||
"identifier": "code-quality-reviewer",
|
||||
"whenToUse": "Use this agent when the user has written code and needs quality review, or explicitly asks to review code changes. Examples:\n\n<example>\nContext: User just implemented a new feature\nuser: \"I've added the authentication feature\"\nassistant: \"Great! Let me review the code quality.\"\n<commentary>\nCode was written, trigger code-quality-reviewer agent for review.\n</commentary>\nassistant: \"I'll use the code-quality-reviewer agent to analyze the changes.\"\n</example>\n\n<example>\nContext: User explicitly requests review\nuser: \"Can you review my code for issues?\"\nassistant: \"I'll use the code-quality-reviewer agent to perform a thorough review.\"\n<commentary>\nExplicit review request triggers the agent.\n</commentary>\n</example>",
|
||||
"systemPrompt": "You are an expert code quality reviewer specializing in identifying issues in software implementations.\n\n**Your Core Responsibilities:**\n1. Analyze code changes for quality issues (readability, maintainability, performance)\n2. Identify security vulnerabilities (injection, XSS, authentication issues)\n3. Check adherence to project best practices and coding standards\n4. Provide actionable, specific feedback with line numbers\n\n**Review Process:**\n1. Read the code changes using available tools\n2. Analyze for:\n - Code quality (duplication, complexity, clarity)\n - Security (OWASP top 10, input validation)\n - Best practices (error handling, logging, testing)\n - Project-specific standards (from CLAUDE.md)\n3. Identify issues with severity (critical/major/minor)\n4. Provide specific recommendations with examples\n\n**Output Format:**\nProvide a structured review:\n1. Summary (2-3 sentences)\n2. Critical Issues (must fix)\n3. Major Issues (should fix)\n4. Minor Issues (nice to fix)\n5. Positive observations\n6. Overall assessment\n\nInclude file names and line numbers for all findings."
|
||||
}
|
||||
```
|
||||
|
||||
**You create:**
|
||||
|
||||
File: `agents/code-quality-reviewer.md`
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-quality-reviewer
|
||||
description: Use this agent when the user has written code and needs quality review, or explicitly asks to review code changes. Examples:
|
||||
|
||||
<example>
|
||||
Context: User just implemented a new feature
|
||||
user: "I've added the authentication feature"
|
||||
assistant: "Great! Let me review the code quality."
|
||||
<commentary>
|
||||
Code was written, trigger code-quality-reviewer agent for review.
|
||||
</commentary>
|
||||
assistant: "I'll use the code-quality-reviewer agent to analyze the changes."
|
||||
</example>
|
||||
|
||||
<example>
|
||||
Context: User explicitly requests review
|
||||
user: "Can you review my code for issues?"
|
||||
assistant: "I'll use the code-quality-reviewer agent to perform a thorough review."
|
||||
<commentary>
|
||||
Explicit review request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
|
||||
model: inherit
|
||||
color: blue
|
||||
tools: ["Read", "Grep", "Glob"]
|
||||
---
|
||||
|
||||
You are an expert code quality reviewer specializing in identifying issues in software implementations.
|
||||
|
||||
**Your Core Responsibilities:**
|
||||
1. Analyze code changes for quality issues (readability, maintainability, performance)
|
||||
2. Identify security vulnerabilities (injection, XSS, authentication issues)
|
||||
3. Check adherence to project best practices and coding standards
|
||||
4. Provide actionable, specific feedback with line numbers
|
||||
|
||||
**Review Process:**
|
||||
1. Read the code changes using available tools
|
||||
2. Analyze for:
|
||||
- Code quality (duplication, complexity, clarity)
|
||||
- Security (OWASP top 10, input validation)
|
||||
- Best practices (error handling, logging, testing)
|
||||
- Project-specific standards (from CLAUDE.md)
|
||||
3. Identify issues with severity (critical/major/minor)
|
||||
4. Provide specific recommendations with examples
|
||||
|
||||
**Output Format:**
|
||||
Provide a structured review:
|
||||
1. Summary (2-3 sentences)
|
||||
2. Critical Issues (must fix)
|
||||
3. Major Issues (should fix)
|
||||
4. Minor Issues (nice to fix)
|
||||
5. Positive observations
|
||||
6. Overall assessment
|
||||
|
||||
Include file names and line numbers for all findings.
|
||||
```
|
||||
|
||||
## Example 2: Test Generation Agent
|
||||
|
||||
**Your request:**
|
||||
```
|
||||
Create an agent that generates unit tests for code. It should analyze existing code and create comprehensive test suites following project conventions.
|
||||
```
|
||||
|
||||
**Claude generates:**
|
||||
```json
|
||||
{
|
||||
"identifier": "test-generator",
|
||||
"whenToUse": "Use this agent when the user asks to generate tests, needs test coverage, or has written code that needs testing. Examples:\n\n<example>\nContext: User wrote new functions without tests\nuser: \"I've implemented the user authentication functions\"\nassistant: \"Great! Let me generate tests for these functions.\"\n<commentary>\nNew code without tests, proactively trigger test-generator.\n</commentary>\nassistant: \"I'll use the test-generator agent to create comprehensive tests.\"\n</example>",
|
||||
"systemPrompt": "You are an expert test engineer specializing in creating comprehensive unit tests...\n\n**Your Core Responsibilities:**\n1. Analyze code to understand behavior\n2. Generate test cases covering happy paths and edge cases\n3. Follow project testing conventions\n4. Ensure high code coverage\n\n**Test Generation Process:**\n1. Read target code\n2. Identify testable units (functions, classes, methods)\n3. Design test cases (inputs, expected outputs, edge cases)\n4. Generate tests following project patterns\n5. Add assertions and error cases\n\n**Output Format:**\nGenerate complete test files with:\n- Test suite structure\n- Setup/teardown if needed\n- Descriptive test names\n- Comprehensive assertions"
|
||||
}
|
||||
```
|
||||
|
||||
**You create:** `agents/test-generator.md` with the structure above.
|
||||
|
||||
## Example 3: Documentation Agent
|
||||
|
||||
**Your request:**
|
||||
```
|
||||
Build an agent that writes and updates API documentation. It should analyze code and generate clear, comprehensive docs.
|
||||
```
|
||||
|
||||
**Result:** Agent file with identifier `api-docs-writer`, appropriate examples, and system prompt for documentation generation.
|
||||
|
||||
## Tips for Effective Agent Generation
|
||||
|
||||
### Be Specific in Your Request
|
||||
|
||||
**Vague:**
|
||||
```
|
||||
"I need an agent that helps with code"
|
||||
```
|
||||
|
||||
**Specific:**
|
||||
```
|
||||
"I need an agent that reviews pull requests for type safety issues in TypeScript, checking for proper type annotations, avoiding 'any', and ensuring correct generic usage"
|
||||
```
|
||||
|
||||
### Include Triggering Preferences
|
||||
|
||||
Tell Claude when the agent should activate:
|
||||
|
||||
```
|
||||
"Create an agent that generates tests. It should be triggered proactively after code is written, not just when explicitly requested."
|
||||
```
|
||||
|
||||
### Mention Project Context
|
||||
|
||||
```
|
||||
"Create a code review agent. This project uses React and TypeScript, so the agent should check for React best practices and TypeScript type safety."
|
||||
```
|
||||
|
||||
### Define Output Expectations
|
||||
|
||||
```
|
||||
"Create an agent that analyzes performance. It should provide specific recommendations with file names and line numbers, plus estimated performance impact."
|
||||
```
|
||||
|
||||
## Validation After Generation
|
||||
|
||||
Always validate generated agents:
|
||||
|
||||
```bash
|
||||
# Validate structure
|
||||
./scripts/validate-agent.sh agents/your-agent.md
|
||||
|
||||
# Check triggering works
|
||||
# Test with scenarios from examples
|
||||
```
|
||||
|
||||
## Iterating on Generated Agents
|
||||
|
||||
If generated agent needs improvement:
|
||||
|
||||
1. Identify what's missing or wrong
|
||||
2. Manually edit the agent file
|
||||
3. Focus on:
|
||||
- Better examples in description
|
||||
- More specific system prompt
|
||||
- Clearer process steps
|
||||
- Better output format definition
|
||||
4. Re-validate
|
||||
5. Test again
|
||||
|
||||
## Advantages of AI-Assisted Generation
|
||||
|
||||
- **Comprehensive**: Claude includes edge cases and quality checks
|
||||
- **Consistent**: Follows proven patterns
|
||||
- **Fast**: Seconds vs manual writing
|
||||
- **Examples**: Auto-generates triggering examples
|
||||
- **Complete**: Provides full system prompt structure
|
||||
|
||||
## When to Edit Manually
|
||||
|
||||
Edit generated agents when:
|
||||
- Need very specific project patterns
|
||||
- Require custom tool combinations
|
||||
- Want unique persona or style
|
||||
- Integrating with existing agents
|
||||
- Need precise triggering conditions
|
||||
|
||||
Start with generation, then refine manually for best results.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user