Compare commits

...

37 Commits

Author SHA1 Message Date
Roy Arsan
6234fa8f14 Add Claude Gateway on GCP example deployment assets
Reference artifacts accompanying the Gateway-on-GCP walkthrough
(https://code.claude.com/docs/en/claude-apps-gateway-on-gcp), including
its Terraform reference section:

- setup.sh: scripts the walkthrough end to end via gcloud (APIs, service
  account, image build/push, private-IP Cloud SQL, secrets, Cloud Run)
- Dockerfile: runtime image for the gateway; setup.sh downloads the
  public release binary and verifies it against the release manifest
- gateway.yaml.example: config template (Vertex upstream, Google
  Workspace IdP)
- terraform/: module provisioning the same architecture (Cloud Run track)

Provided as a working example to adapt, not a supported production
deployment.
2026-06-29 21:37:59 +00:00
GitHub Actions
01f1617f14 chore: Update CHANGELOG.md and feed.xml 2026-06-26 21:29:36 +00:00
GitHub Actions
f0919a1a72 chore: Update CHANGELOG.md and feed.xml 2026-06-25 21:45:51 +00:00
GitHub Actions
0bd954331e chore: Update CHANGELOG.md and feed.xml 2026-06-24 21:58:06 +00:00
GitHub Actions
5c1517a21b chore: Update CHANGELOG.md and feed.xml 2026-06-24 15:53:49 +00:00
GitHub Actions
2aa6ef3d35 chore: Update CHANGELOG.md and feed.xml 2026-06-23 21:03:40 +00:00
GitHub Actions
12281998d8 chore: Update CHANGELOG.md and feed.xml 2026-06-22 20:37:27 +00:00
GitHub Actions
b4073894cd chore: Update CHANGELOG.md and feed.xml 2026-06-20 20:59:12 +00:00
GitHub Actions
c487902a53 chore: Update CHANGELOG.md and feed.xml 2026-06-19 01:20:44 +00:00
Ashwin Bhat
baf38ddaaa Fix lock-closed-issues workflow: use search API instead of offset pagination (#69470)
The workflow has been failing daily since 2026-04-27 with HTTP 422
"Pagination with the page parameter is not supported for large
datasets" at page=100. The repo now has ~58k closed issues and the
script was paging past ~10k already-locked ones every run before
reaching any candidates.

Replace listForRepo + page=N with the search API
(is:issue is:closed is:unlocked updated:<cutoff), which returns only
the issues that actually need locking. Cap at 250/run with a 1s sleep
between locks to stay under secondary rate limits.

Claude-Session: https://claude.ai/code/session_016EWY3FKCJyfUdCAZkXfi7i
2026-06-18 17:15:49 -07:00
GitHub Actions
4fa369b5b3 chore: Update CHANGELOG.md and feed.xml 2026-06-18 22:03:35 +00:00
william qian
423563cfe3 Update frontend-design skill, bump plugin to 1.1.0 (#69226) 2026-06-17 19:25:44 -07:00
GitHub Actions
0047022a46 chore: Update CHANGELOG.md and feed.xml 2026-06-17 22:07:35 +00:00
GitHub Actions
843959fad9 chore: Update CHANGELOG.md and feed.xml 2026-06-16 20:44:34 +00:00
GitHub Actions
1b7380874c chore: Update CHANGELOG.md and feed.xml 2026-06-16 20:22:06 +00:00
GitHub Actions
64ceb97caa chore: Update CHANGELOG.md and feed.xml 2026-06-15 21:35:48 +00:00
GitHub Actions
ca9f6045fc chore: Update CHANGELOG.md and feed.xml 2026-06-12 21:53:22 +00:00
GitHub Actions
ee81682a72 chore: Update CHANGELOG.md and feed.xml 2026-06-12 04:23:45 +00:00
GitHub Actions
5754a8bd4f chore: Update CHANGELOG.md and feed.xml 2026-06-12 01:16:30 +00:00
GitHub Actions
3a7c736101 chore: Update CHANGELOG.md and feed.xml 2026-06-11 05:41:48 +00:00
GitHub Actions
ca34f27543 chore: Update CHANGELOG.md and feed.xml 2026-06-10 20:44:09 +00:00
GitHub Actions
1c5f951a48 chore: Update CHANGELOG.md and feed.xml 2026-06-09 22:14:36 +00:00
GitHub Actions
6a9c2dbe45 chore: Update CHANGELOG.md and feed.xml 2026-06-09 17:23:03 +00:00
GitHub Actions
f967b36c1b chore: Update CHANGELOG.md and feed.xml 2026-06-08 21:57:11 +00:00
GitHub Actions
72281753c2 chore: Update CHANGELOG.md and feed.xml 2026-06-06 23:41:47 +00:00
GitHub Actions
c1b75cba5e chore: Update CHANGELOG.md and feed.xml 2026-06-06 01:33:29 +00:00
GitHub Actions
6988846f0f chore: Update CHANGELOG.md and feed.xml 2026-06-06 00:55:13 +00:00
GitHub Actions
feabcc3c2b chore: Update CHANGELOG.md and feed.xml 2026-06-05 05:45:00 +00:00
GitHub Actions
d1e174252d chore: Update CHANGELOG.md and feed.xml 2026-06-04 21:52:46 +00:00
GitHub Actions
b67fa4fa2c chore: Update CHANGELOG.md and feed.xml 2026-06-03 21:31:29 +00:00
GitHub Actions
625c04c335 chore: Update CHANGELOG.md and feed.xml 2026-06-02 21:58:16 +00:00
GitHub Actions
bdb04fc524 chore: Update CHANGELOG.md and feed.xml 2026-06-02 02:10:17 +00:00
GitHub Actions
8bae02d531 chore: Update CHANGELOG.md and feed.xml 2026-05-31 19:42:42 +00:00
GitHub Actions
295dee881d chore: Update CHANGELOG.md and feed.xml 2026-05-30 02:42:09 +00:00
GitHub Actions
8d0fbf451a chore: Update CHANGELOG.md and feed.xml 2026-05-29 20:20:32 +00:00
GitHub Actions
2d5c3c6c85 chore: Update CHANGELOG.md and feed.xml 2026-05-29 01:42:17 +00:00
GitHub Actions
1696f22294 chore: Update CHANGELOG.md and feed.xml 2026-05-28 18:00:48 +00:00
19 changed files with 2656 additions and 657 deletions

View File

@@ -72,7 +72,7 @@
{
"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.0.0",
"version": "1.1.0",
"author": {
"name": "Prithvi Rajasekaran & Alexander Bricken",
"email": "prithvi@anthropic.com"

View File

@@ -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}`);

View File

@@ -1,5 +1,594 @@
# Changelog
## 2.1.195
- Added `CLAUDE_CODE_DISABLE_MOUSE_CLICKS` to disable mouse click/drag/hover in fullscreen mode while keeping wheel scroll
- Fixed hook matchers with hyphenated identifiers (e.g. `code-reviewer`, `mcp__brave-search`) accidentally substring-matching — they now exact-match. Use `mcp__brave-search__.*` to match all tools from a hyphenated MCP server.
- Fixed voice dictation on macOS capturing silence in long-running sessions after the default input device changes
- Fixed voice dictation auto-submit never firing for languages written without spaces (Japanese, Chinese, Thai)
- Fixed external plugins enabled only by project `.claude/settings.json` not requiring explicit install consent on every loader path
- Fixed `/plugin` Enable/Disable not working when a plugin's `plugin.json` `name` differs from its marketplace entry name
- Fixed background jobs disappearing from `claude agents` or losing data when written by a newer Claude Code version
- Fixed reopening a crashed background task showing a blank screen for up to 5 seconds instead of its restart
- Fixed background agent daemons running unreachable when the control socket fails to start, blocking restarts
- Improved voice mode on Linux: now distinguishes "no microphone" from "SoX not installed" when SoX is present but no audio capture device exists
- Improved `claude agents` completed list to fill available vertical space; on short terminals the header compacts so live sessions stay visible
- Improved Remote session startup with a provisioning checklist while the container starts
## 2.1.193
- Added `autoMode.classifyAllShell` setting to route all Bash/PowerShell commands through the auto-mode classifier instead of only arbitrary-code-execution patterns
- Added auto-mode denial reasons to the transcript, the denial toast, and `/permissions` recent denials
- Added `claude_code.assistant_response` OpenTelemetry log event containing the model's response text. Redacted unless `OTEL_LOG_ASSISTANT_RESPONSES=1`; when that var is unset it follows `OTEL_LOG_USER_PROMPTS`, so deployments that already log prompt content will start receiving response content on upgrade — set `OTEL_LOG_ASSISTANT_RESPONSES=0` to keep prompts-only.
- Added live file path autocomplete to bash mode (`!`)
- Added a startup notice when MCP servers need authentication, pointing at `/mcp`
- Added automatic memory-pressure reaping for idle background shell commands (disable with `CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP=1`)
- Fixed `/model` and other client-data-gated UI showing stale/empty state immediately after `/login`
- Fixed backgrounding (←←) spuriously cancelling with "N background tasks would be abandoned" when all running tasks carry over to the new session
- Fixed pinned background agents being re-prompted to "Continue from where you left off" after every auto-update
- Fixed backgrounding the main turn spawning a phantom "general-purpose (resumed)" subagent that re-ran the main conversation
- Fixed agent panel hiding sibling agents when viewing a subagent
- Improved background agents: the launch result no longer instructs Claude to "end your response" — it keeps working on other tasks while the agent runs
- Improved MCP `headersHelper` auth: the helper now re-runs and reconnects automatically when a tool call returns 401/403
- Improved plugin auto-rename: marketplace `renames` maps are now followed automatically, updating your settings to the new name
- Improved `/add-dir` message when the directory is already a working directory
## 2.1.191
- Added `/rewind` support for resuming a conversation from before `/clear` was run
- Fixed scroll position jumping to the bottom while reading earlier output during a streaming response
- Fixed background agents resurrecting after being stopped — stopping an agent from the tasks panel is now permanent
- Fixed `/voice` showing a generic "not available" message when disabled by an organization's policy — it now explains the restriction
- Fixed `/login` URL opening truncated in Windows Terminal when it wraps across lines
- Fixed Cmd+click on links in fullscreen mode for Ghostty over ssh/tmux
- Fixed `claude agents` sending builtin slash commands like `/usage` to background sessions as prompt text instead of showing a hint
- Fixed `claude agents` job rows showing full filesystem paths for pasted images instead of the `[Image #N]` placeholder
- Fixed hooks with comma-separated matchers (e.g. `"Bash,PowerShell"`) silently never firing
- Fixed `/permissions` Recently-denied tab: approving a denial now persists on close instead of being silently discarded
- Fixed the agent panel jumping by one row when scrolling the roster past the overflow cap
- Fixed the welcome splash art overflowing the default 80×24 macOS Terminal window
- Fixed managed settings: `forceRemoteSettingsRefresh` now takes effect when set via MDM or file policy, and the fetch sends `Cache-Control: no-cache` to prevent proxies from serving stale responses
- Improved sandbox network permission dialog: hosts you allow with "Yes" are now remembered for the rest of the session instead of re-prompting on every connection
- Improved MCP server reliability: capability discovery (`tools/list`, `prompts/list`, `resources/list`) now retries transient network errors with short backoff
- Improved MCP OAuth: discovery and token requests now retry once after transient network errors, and headless environments skip the browser popup and go straight to the paste-the-URL prompt
- Improved MCP error messages: HTTP 404 errors now show the URL and point to your MCP config
- Improved vim mode prompt-history search (NORMAL `/`) to hint how to reach slash commands
- Reduced CPU usage during streaming responses by ~37% by coalescing text updates to 100ms
- Reduced long-session memory growth from terminal output cache
## 2.1.190
- Bug fixes and reliability improvements
## 2.1.187
- Added `sandbox.credentials` setting to block sandboxed commands from reading credential files and secret environment variables
- Added org-configured model restrictions to the model picker, `--model`, `/model`, and `ANTHROPIC_MODEL`, with a "restricted by your organization's settings" message when a restricted model is selected
- Added mouse click support to select menus (permission prompts, `/model`, `/config`, etc.) in fullscreen mode
- Fixed `--resume` failing with "No conversation found" when the original `-p` run produced no model turns
- Fixed `--json-schema` and workflow `agent({schema})` structured output: the model can no longer re-call `StructuredOutput` indefinitely after a successful call, and follow-up turns now reliably return structured output
- Fixed remote MCP tool calls that hang with no response for 5 minutes — they now abort with an error instead of blocking indefinitely (override with `CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT`)
- Fixed Claude Code Remote sessions taking ~2.7s longer to start after the agent proxy CA system-trust install was added
- Fixed pasted Korean/CJK text turning into mojibake in terminals that deliver paste as per-byte extended-key events
- Fixed `/update` over Remote Control hanging when a startup trust dialog would have shown
- Fixed background jobs in the agents view getting stuck in "working" indefinitely when the agent ended a turn without producing structured output
- Fixed channel connections dropping after navigating to the agents view and back, and after `/bg`, `/tui`, or `/update`
- Fixed agent stop notifications not correctly attributing who stopped the agent, and improved wording ("finished"/"stopped" instead of "came to rest")
- Fixed subagent depth tracking: resumed subagents now restore their original spawn depth, and forked subagents now count toward the depth cap
- Fixed leaked agent worktree registrations: locked `.git/worktrees/` entries from killed agents are now cleaned up automatically
- Fixed Cmd+click not opening URLs in fullscreen mode in Ghostty on macOS
- Fixed `claude --help` not listing the `--bg`/`--background` flag
- Fixed Esc, Ctrl-C, and Ctrl-D not working while `/share` is uploading
- Improved `/install-github-app`: GitHub Actions workflow setup is now optional — you can install just the GitHub App and skip the workflow/secret steps
- Improved `/btw` with ←/→ arrow navigation to step through earlier answers
- Improved `/plugin` to surface plugins you haven't used recently so you can clean them up
- [VSCode] Fixed extension becoming unresponsive when resuming a large session
## 2.1.186
- Added `claude mcp login <name>` and `claude mcp logout <name>` to authenticate MCP servers from the CLI without opening the interactive `/mcp` menu, with `--no-browser` stdin redirect support for completing over SSH
- Added status filtering (press `f`) to the `/workflows` agent detail view
- Added a "Skills" section to the `/plugin` Installed tab
- Added `teammateMode: "iterm2"` setting with a warning when auto mode cannot find the `it2` CLI
- Added "Claude Platform on AWS - refresh credentials" option to `/login` when `awsAuthRefresh` is configured
- `!` bash commands now trigger Claude to respond to the output automatically; set `"respondToBashCommands": false` in settings.json to keep the previous context-only behavior
- Fixed streaming requests failing with "Content block not found" or JSON parse errors after the machine wakes from sleep
- Fixed subagent transcript scroll position bleeding into the main transcript on exit
- Fixed background task previews flashing raw tool names before the agent's plan loaded
- Fixed Chrome tab-group isolation not applying when the in-product permissions gate is off for concurrent CLI sessions
- Fixed background session recaps being duplicated; the agent's own end-of-turn summary now shows as the recap line
- Fixed opening a background session from `claude agents` leaving the previous screen painted behind it
- Fixed `Agent(type)` deny rules and `Agent(x,y)` allowed-types restrictions not being enforced for named subagent spawns
- Fixed Esc and Ctrl+C not responding while background agents are still running after the main turn ends
- Fixed misaligned option numbers in permission prompts when the option text overflows
- Fixed pressing `x` on a finished subagent in the agent panel not dismissing it
- Fixed a misleading "MCP server disconnected" notice for intentionally retired tools when resuming older sessions
- Fixed `/plugin` Installed showing a "more above" indicator when already scrolled to the top
- Fixed `~~strikethrough~~` showing literal tildes in assistant messages instead of rendering as strikethrough
- Fixed `--tools` allowing feature-gated tools to slip through before flags loaded on a cold first launch
- Fixed background job status in `claude agents` showing a stale "needs input" message after replying
- Fixed a dark-theme flash when opening a background session from `claude agents` on a light terminal
- Fixed mouse-selected text staying highlighted after deleting it in `claude agents`
- Fixed session cost not showing for usage-based Enterprise and Team subscribers
- Fixed agent teams: teammates spawned via tmux/pane backends now inherit the leader's `--effort` level
- Fixed Workflow `agent({schema})` subagents looping forever on repeated schema validation failures instead of aborting after 5 attempts
- Improved `claude mcp get` and `claude mcp remove` to suggest the closest configured server name on a typo and truncate long server lists
- Improved memory: the agent is now reminded to compact its `MEMORY.md` index when nearing the size limit
- Improved skill frontmatter: `display-name`, `default-enabled`, `fallback`, and `metadata.*` keys now accept kebab-case, snake_case, and camelCase
- Improved malformed `SKILL.md` YAML frontmatter handling: loads the skill body with empty metadata instead of failing silently
- Changed `CLAUDE_CODE_MAX_RETRIES` to cap at 15; for unattended sessions, use `CLAUDE_CODE_RETRY_WATCHDOG` instead
- Changed background subagents to surface permission prompts in the main session instead of auto-denying; the dialog shows which agent is asking, and Esc denies just that tool
- Changed `/review <pr>` to use the same review engine as `/code-review medium`
## 2.1.185
- The stream-stall hint now reads "Waiting for API response · will retry in …" instead of "No response from API · Retrying in …", and triggers after 20s of silence instead of 10s
## 2.1.183
- Improved auto mode safety: destructive git commands (`git reset --hard`, `git checkout -- .`, `git clean -fd`, `git stash drop`) are now blocked when you didn't ask to discard local work, `git commit --amend` is blocked when the commit wasn't made by the agent this session, and `terraform destroy`/`pulumi destroy`/`cdk destroy` are blocked unless you asked for the specific stack
- Added a warning when the requested model is deprecated or automatically updated to a newer model, shown on stderr in print mode (`-p`) and now also covering models set in agent frontmatter
- Added `attribution.sessionUrl` setting to omit the claude.ai session link from commits and PRs in web and Remote Control sessions
- Added `/config --help` to list all available shorthand keys for `/config key=value`
- Changed `/config` toggle behavior: Enter and Space both change the selected setting, and Esc now saves and closes instead of reverting
- Removed the startup "setup issues" line under the logo — run `/doctor` to see configuration issues or use `--debug`
- Fixed `thinking.disabled.display: Extra inputs are not permitted` 400 errors on subagent spawns and session-title generation for affected configurations
- Fixed WebSearch returning empty results in subagents
- Fixed the terminal cursor being stranded above the prompt after navigating history in vim mode with the native cursor enabled
- Fixed fullscreen TUI corruption (statusline mid-screen, duplicated spinner rows, merged text) in Windows Terminal under heavy nested-subagent load
- Fixed turns silently completing with no visible output when the model returned only a thinking block; Claude now re-prompts once
- Fixed user-level skills appearing multiple times in slash-command autocomplete when multiple plugins are enabled
- Fixed MCP servers requiring authentication exposing auth-stub tools to the model in headless/SDK mode
- Fixed tmux teammate panes failing to launch when the shell has slow rc-file initialization, and keystrokes typed during agent spawn leaking into the new tmux pane instead of the leader prompt
- Fixed background tasks started by a teammate being killed when the teammate finishes a turn
- Fixed scheduled task and webhook trigger deliveries being treated as keyboard input; they now classify as task notifications and can no longer approve a pending action or set the session title in auto mode
- Fixed focus mode showing "Ran N PostToolUse hooks" timing lines under each response
## 2.1.181
- Added `/config key=value` syntax to set any setting from the prompt (e.g. `/config thinking=false`) — works in interactive, `-p`, and Remote Control
- Added `sandbox.allowAppleEvents` opt-in setting that lets sandboxed commands send Apple Events on macOS
- Added `CLAUDE_CLIENT_PRESENCE_FILE` environment variable: point it at a marker file to suppress mobile push notifications while you're at the machine
- Upgraded the bundled Bun runtime to 1.4
- Improved streaming of long paragraphs: text now appears line-by-line instead of waiting for the first line break
- Improved auto-retry: API connection drops mid-thinking now automatically retry instead of showing "Connection closed while thinking"
- Improved the subagent panel: idle subagents auto-hide after 30s, the list caps at 5 rows with scroll hints, and keyboard hints now show in the footer
- Improved the MCP OAuth browser page to match Claude Code's visual style and auto-close on success
- Changed fullscreen mode URL opening to require Cmd+click (macOS) / Ctrl+click, matching native terminal behavior
- Changed the `Improved N memories` line to no longer list individual files outside verbose mode
- Fixed prompt caching not reading on custom `ANTHROPIC_BASE_URL` and on Foundry due to a per-request attestation token changing every turn
- Fixed Write/Edit producing 0-byte or truncated files on network drives and cloud-synced folders
- Fixed `open`, `osascript`, and browser-based auth flows failing with error -600 on macOS by adding the Apple Events entitlement
- Fixed a startup regression (~120ms per launch in fresh environments, introduced in 2.1.169): the first prompt no longer waits for the managed-settings fetch when no MCP servers are configured
- Fixed startup blocking with a blank terminal for up to 15 seconds when the account settings fetch is slow on a degraded network
- Fixed startup crash (`TypeError: Cannot read properties of null`) when `.claude.json` contains corrupted null project entries
- Fixed macOS TUI freezing at session start (Ctrl+C unresponsive) when Spotlight is busy reindexing
- Fixed long-running idle sessions losing their history when another Claude Code process ran the 30-day transcript cleanup
- Fixed foreground subagents spawning unbounded nested chains; they now respect the same 5-level depth limit as background subagents
- Fixed `/recap` and conversation forks using the previous model immediately after a model switch
- Fixed subagent "Thinking" duration showing the parent agent's elapsed time instead of the subagent's own
- Fixed subagents blocked on a nested agent showing a ticking elapsed time instead of "waiting" in the agent panel
- Fixed the API retry indicator ("Retrying in 0s · attempt N/10") staying on screen after the retry succeeded
- Fixed AWS `awsCredentialExport` credentials with a short remaining lifetime causing credential refreshes every minute, and now accepts the JSON shape from `aws configure export-credentials`
- Fixed `claude mcp get`/`list` showing `✓ Connected` when tools/list fails; they now show `! Connected · tools fetch failed` with the error detail
- Fixed `/remote-control` leaving a stale "connecting…" line; it now confirms in the transcript once connected
- Fixed ExitWorktree refusing to remove a clean worktree with "Could not verify worktree state" when bare `git` cannot be resolved on Windows
- Fixed settings changes (such as `/effort` or `/model`) failing with ENOENT when `~/.claude/settings.json` is a relative symlink under a symlinked `~/.claude`
- Fixed IDE selection line numbers in context reminders being off by one (IntelliJ and VS Code)
- Fixed Ctrl+C in fullscreen after a native terminal selection (modifier+drag) overwriting the clipboard with the app's prior selection
- Fixed Ctrl+V showing "No image found in clipboard" instead of pasting when the clipboard contains text
- Fixed agent creation failing with "EEXIST: file already exists" when the agents directory already exists (Windows/OneDrive)
- Fixed AskUserQuestion preview content being cut off at the dialog edge instead of word-wrapping
- Fixed AskUserQuestion multi-select questions silently dropping a typed "Other" free-text answer when submitting
- Fixed `/stats` "Most active day" and daily token chart dates showing one day early in UTC-negative timezones
- Fixed `/copy` and copy-on-select on Linux not detecting a clipboard utility installed after Claude Code started
- Fixed tab-indented code rendering with incorrect indentation in the Write (create-file) preview
- Fixed user prompts queued mid-turn not showing a full-width background highlight in the transcript
- Fixed the activity spinner's pulse dwelling on the wrong glyph size in Ghostty
## 2.1.179
- Fixed mid-stream connection drops: partial responses are now preserved instead of showing a raw error, and the spinner no longer gets stuck at "running tool"
- Fixed mouse-wheel scrolling in WSL2 under Windows Terminal and VS Code (regression in 2.1.172)
- Fixed a sandbox `denyRead`/`allowRead` glob over a large directory tree making the Bash tool description enormous and the session unusable on Linux
- Fixed the feedback survey capturing a single-digit reply as a session rating immediately after a turn completes
- Fixed the welcome screen stacking multiple promotional banners — at most one promo now shows per session
- Fixed Ctrl+O not showing the subagent's transcript when viewing a subagent
- Fixed clicking the prompt input not returning focus from the subagent/footer panel
- Fixed remote session background tasks appearing stuck as "still running" between turns
- Improved plugin loading performance in remote sessions
## 2.1.178
- Agent teams: removed the `TeamCreate` and `TeamDelete` tools. With `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` set, every session now has one implicit team — spawn teammates directly with the Agent tool's `name` parameter, no setup step needed. The `team_name` parameter on the Agent tool is still accepted but ignored.
- Added `Tool(param:value)` syntax for permission rules to match a tool's input parameters (with `*` wildcard), e.g. `Agent(model:opus)` to block Opus subagents
- Skills in nested `.claude/skills` directories now load when working on files there; on a name clash, the nested skill appears as `<dir>:<name>` so both stay available
- Nested `.claude/` directories: the agent, workflow, and output-style closest to the working directory now wins when names collide; project-scope workflow saves now target the closest existing `.claude/workflows/`
- Improved auto mode: subagent spawns are now evaluated by the classifier before launch, closing a gap where a subagent could request a blocked action without review
- Improved `/doctor` with consistent flat tree layout across all sections, clearer section status icons, and highlighted command names
- Improved the skill listing truncation warning to show how many skill descriptions are affected
- Changed the workflow prompt keyword to use a purple shimmer highlight and trigger only on explicit phrases like "run a workflow" or "workflow:", not on any mention of the word
- Improved Remote Control error messages: connection failures now show a persistent red "/rc failed" indicator in the footer, and the "not yet enabled" error now explains whether it's a gate, a check failure, stale entitlement, or org policy
- `/bug` now requires a description before submitting, and no longer uses model-refusal text as the GitHub issue title
- Fixed a crash (out-of-memory) when the CLI inherits a stale websocket/OAuth file-descriptor environment variable from a parent process
- Fixed Claude in Chrome silently failing to connect when the OAuth token belongs to a different account than the Claude Code login
- Fixed nested `.claude/skills` skills with directory-qualified names being blocked by permission prompts in non-interactive runs
- Fixed several subagent issues: viewing a subagent's transcript now shows tool results and live progress, messages sent while it finishes its turn are no longer dropped, and backgrounding a running subagent (ctrl+b) no longer restarts it from scratch
- Fixed `claude agents` workers failing with `401 Invalid bearer token` when the daemon was started from a shell with a custom API gateway via `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN`
- Fixed compaction not honoring `--fallback-model`: compaction now falls back to the configured fallback model chain on overload or model-availability errors
- Fixed model requests continuing to fail with auth errors after credentials were refreshed outside the session, due to a stale cached request configuration
- Fixed background sessions created with `/bg` or `←←` after a turn finished showing "Working" forever in the agents list
- Fixed Linux sandbox failing to start when `.claude/skills` or `.claude/hooks` is a symlink
- Fixed `CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE=1` preventing fresh marketplace installs from cloning
- Fixed MCP server-level specs (`mcp__server`, `mcp__server__*`, `mcp__*`) in subagent `disallowedTools` being silently ignored
- Fixed vim mode undo: `u` now steps through NORMAL/VISUAL-mode commands one at a time instead of merging commands in quick succession into a single undo step
- Fixed statusline links with custom URI schemes (e.g. `vscode://`) not opening when clicked in `claude agents`
- [VSCode] Fixed pressing Esc to dismiss a CJK IME candidate window canceling the running Claude task
## 2.1.176
- Session titles are now generated in the language of your conversation (set the `language` setting to pin a specific language)
- Added `footerLinksRegexes` setting for regex-matched link badges in the footer row, configurable via user or managed settings
- Improved Bedrock credential caching: credentials from `awsCredentialExport` are now cached until their `Expiration` instead of a fixed 1 hour
- Fixed `availableModels` enforcement: alias model picks can no longer be redirected to a blocked model via `ANTHROPIC_DEFAULT_*_MODEL` environment variables, and `/fast` now refuses to toggle when it would switch to a model outside the allowlist
- Fixed auto mode failing on Fable 5 for organizations without Opus 4.8 enabled — the classifier now falls back to the best available Opus model
- Fixed hook `if` conditions for Read/Edit/Write tool paths: documented patterns like `Edit(src/**)`, `Read(~/.ssh/**)`, and `Read(.env)` now match correctly
- Fixed Linux sandbox failing to start when `.claude/settings.json` is a symlink with an absolute target
- Fixed `/copy` and mouse-selection copy not reaching the system clipboard inside tmux over SSH, and tmux paste buffer not loading on versions older than 3.2
- Fixed Remote Control connecting from web/mobile silently switching the session's model
- Fixed Remote Control disconnect notifications showing a bare numeric code instead of a human-readable reason, and connection failures adding a duplicate line to the conversation transcript
- Fixed Remote Control sessions not disconnecting when you sign in to a different account
- Fixed `/cd` and worktree moves leaving the session reporting the previous directory's git branch
- Fixed `claude agents`: pressing back in one window no longer detaches other windows attached to the same session
- Fixed backgrounded sessions showing "Working" forever when `/bg` mid-turn had nothing left to continue
- Fixed background agent search by PR URL: PRs opened during scheduled wakeups or while a job was blocked now appear in `claude agents` search
- Fixed the agents view input showing no text cursor on Windows
- Fixed `claude --bg -cn <name>` not seeding the session name
- Fixed background sessions to neutralize Windows network paths in persisted state before respawn
- Fixed background-session respawn rejecting malformed resume IDs from corrupted state files
- Fixed the Windows background-service daemon not starting when `~/.claude/daemon` has the ReadOnly attribute set
- Fixed cloud sessions failing with "Could not resolve authentication method" when idle for too long before being claimed
- Background sessions now show clearer guidance when a window left open across an auto-update can't submit a reply, and `claude daemon status` explains version-skew behavior
## 2.1.175
- Added `enforceAvailableModels` managed setting — when enabled, the `availableModels` allowlist also constrains the Default model (a Default that would resolve to a disallowed model now falls back to the first allowed model), and user or project settings can no longer widen a managed `availableModels` list
## 2.1.174
- Added `wheelScrollAccelerationEnabled` setting to disable mouse-wheel scroll acceleration in fullscreen mode
- Fixed the `/model` picker hiding the model family that Default resolves to — Opus now appears as its own row on Max/Team Premium/Enterprise plans, Sonnet on Pro/Team plans, and Opus on pay-as-you-go API accounts
- Fixed `/model` picker showing a hardcoded Sonnet version label when `ANTHROPIC_DEFAULT_SONNET_MODEL` pins a different Sonnet
- Fixed the "Fable 5 is now consuming usage credits" banner incorrectly showing for enterprise accounts with usage-based billing
- Fixed Bedrock GovCloud regions (`us-gov-*`) deriving the wrong inference profile prefix (`global` instead of `us-gov`), causing 400 errors on derived model IDs
- Fixed background sessions inheriting another session's `ANTHROPIC_*` provider env (gateway URL, custom headers, `/model` aliases) from the shell that started the background daemon
- Fixed a 1-2 second pause when exiting Claude Code shortly after a shell command was interrupted or killed on macOS and Linux
- Fixed git commit co-author attribution showing an incorrect model name for some models
- Fixed the `/advisor` dialog pre-selecting a saved advisor model that is blocked by the `availableModels` allowlist
- Fixed skill hot-reload re-sending the entire skill listing when a single skill changed; only changed skills are now re-announced
- Fixed Workflow tool `agent()` subagents missing per-agent attribution headers
- [VSCode] Added usage attribution to the Account & usage dialog (`/usage`) showing cache misses, long context, subagents, and per-skill/agent/plugin/MCP breakdowns over the last 24h or 7d
- Fixed pre-warmed background workers failing with "Could not resolve authentication method" when claimed after sitting idle
## 2.1.173
- Fixed Fable 5 model names with a `[1m]` suffix not being normalized — Fable 5 includes 1M context by default, so the suffix is now stripped automatically
- Fixed a spurious "sandbox dependencies missing" startup warning on Windows when sandbox was enabled in settings
## 2.1.172
- Sub-agents can now spawn their own sub-agents (up to 5 levels deep)
- Amazon Bedrock now reads the AWS region from `~/.aws` config files when `AWS_REGION` isn't set, matching AWS SDK precedence; `/status` shows where the region came from
- Added a search bar when browsing a marketplace's plugins in `/plugin`
- Added `model` attribute to the `claude_code.lines_of_code.count` OTEL metric
- Fixed sessions using 1M context without usage credits getting permanently stuck — the session now automatically compacts back under the standard context limit
- Fixed a repeating "an image in the conversation could not be processed and was removed" error when the conversation contained multiple images
- Fixed the agents view keeping a session under Working with a busy spinner for up to 30 seconds after the worker replied
- Fixed background agents potentially reading another directory's project settings (`.mcp.json` approvals, trust) when dispatched onto a pre-warmed worker
- Fixed background-session attach failing with EAUTH for sessions started on an older version after the daemon auto-updated
- Fixed a background sub-agent staying stuck as "active" in the agent panel after a nested agent it spawned was stopped
- Fixed `/model` suggestions in the `claude agents` dispatch input rendering with a misleading slash prefix and showing models disabled for your org
- Fixed `availableModels` restrictions not being applied to subagent model overrides, the agent dispatch model picker, and the advisor model
- Fixed `availableModels` allowlists hiding the `/model` picker's Opus and Sonnet 1M rows when entries use version-specific IDs like `claude-opus-4-8`
- Fixed the `/model` picker on Bedrock offering models the provider doesn't serve — selecting one silently switched the session model and lit the selection marker on multiple rows
- Fixed model IDs getting a doubled 1M-context suffix (e.g. `[1M][1m]`) when `ANTHROPIC_DEFAULT_OPUS_MODEL` already includes one
- Fixed `opusplan` model setting not shipping with 1M context in plan mode for entitled users; the `opusplan[1m]` workaround now also correctly switches to Opus in plan mode
- Fixed `WebFetch(domain:*.example.com)` wildcard domain rules never matching subdomains in allow, deny, and ask position, and file permission rules with mid-pattern wildcards (e.g. `Read(secrets-*/config.json)`) being rejected at startup
- Fixed up-arrow prompt history showing the main agent's prompts while a subagent's chat tab is open
- Fixed memory recall not finding mounted team memory stores (`CLAUDE_MEMORY_STORES`) in remote sessions
- Fixed workflow validation rejecting scripts whose prompt strings or comments merely mention `Date.now()`/`Math.random()`
- Disable mouse tracking on Windows consoles that don't fully support it
- Fixed the `/plugin` marketplace list losing its cursor after backing out of a long plugin list, and Esc from the plugin browser returning to the wrong tab
- Improved performance in long conversations by removing redundant message normalization and avoiding full message-history transforms when streaming tool-use state is unchanged
- Reduced idle CPU usage: `/goal` status chip no longer re-renders the terminal at 5 Hz while idle, and fewer UI re-renders while subagents run in parallel
- Improved Claude in Chrome tool loading: browser tools now load in a single batched call instead of one per tool
- Improved the non-interactive Usage Policy refusal message to suggest starting a new session or changing your model
- `/code-review` now keeps the `ultra` option visible when you're not signed in to claude.ai, with an explanation that the cloud review requires a claude.ai account
- Shortened the Remote Control footer indicator to "/rc active" and hid it on narrow terminals
- Stopped promoting `/loop` in remote sessions, where pending loops don't keep the container alive
- [VSCode] Fixed PowerShell tool calls rendering as raw JSON instead of a proper command display and permission dialog, and stripped ANSI escape codes from displayed shell output
## 2.1.170
- Introducing Claude Fable 5: a Mythos-class model that weve made safe for general use. Fables capabilities exceed those of any model weve ever made generally available. Update to version 2.1.170 for access. https://www.anthropic.com/news/claude-fable-5-mythos-5
- Fixed sessions not saving transcripts (and not appearing in --resume) when launched from the VS Code integrated terminal or any shell that inherited Claude Code environment variables.
## 2.1.169
- Self-hosted runner: added a `post-session` lifecycle hook that runs after the session ends and before the workspace is deleted, so you can snapshot uncommitted work or export logs; also made the child-process SIGTERM→SIGKILL window configurable (default unchanged at 5s)
- Added `--safe-mode` flag (and `CLAUDE_CODE_SAFE_MODE`) to start Claude Code with all customizations (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled for troubleshooting
- Added `/cd` command to move a session to a new working directory without breaking the prompt cache mid-session
- Added a `disableBundledSkills` setting and `CLAUDE_CODE_DISABLE_BUNDLED_SKILLS` environment variable to hide bundled skills, workflows, and built-in slash commands from the model
- Fixed Up/Down arrows jumping to command history past the wrapped rows of a long input line — they now move through each visual row first, and history recall enters at the near edge
- Fixed enterprise managed MCP policies (`allowedMcpServers`/`deniedMcpServers`) not being enforced on reconnect, IDE-typed configs, `--mcp-config` servers during the first session after install, or before remote settings loaded; also fixed slow cold starts for orgs without remote settings
- Fixed a ~30-50ms UI stall at the start of each turn for macOS users logged in with claude.ai credentials
- Fixed `claude -p` being slow or appearing to hang on Windows while waiting for the slash-command/skill scan (regression in 2.1.161)
- Fixed Remote Control getting stuck on "reconnecting" after resuming a session when an OAuth token refresh happened at the same time
- Fixed Git Credential Manager's "Connect to GitHub" popup appearing on Windows at startup when background git commands ran without cached credentials
- Fixed footer hints (e.g. "esc to interrupt") not showing for users with a custom statusline
- Fixed stale permission and dialog prompts reappearing every time you reattached to a remote session whose worker had died while waiting on them
- Fixed `claude agents --json` omitting blocked and just-dispatched background sessions; added `--all` to include completed sessions, plus new `id` and `state` fields
- Fixed agents view leaving a stale/garbled frame after navigating back from an agent on WSL in Windows Terminal
- Fixed background agents ignoring project-level settings `env` values (e.g. `ANTHROPIC_MODEL`) when dispatched onto a pre-warmed worker
- Fixed MCPB plugin cache being spuriously invalidated on Windows, causing unnecessary re-extraction
- Fixed plugin `.in_use` PID lock files accumulating without bound; stale markers from crashed sessions are now swept once per day
- Fixed untrusted project settings being able to set OTEL client-certificate paths without trust confirmation
- `/workflows` now opens immediately even while a turn is in progress
- Improved `TaskCreate` reliability: malformed inputs are repaired automatically and validation errors for unloaded tools include the schema
- Improved the error message shown when your organization has disabled API key authentication, with guidance based on where the active API key comes from
- Reduced CPU usage while responses stream and during spinner animations
- Restored a default 5-minute idle timeout on Vertex/Foundry so a stalled stream aborts instead of hanging indefinitely; set `API_FORCE_IDLE_TIMEOUT=0` to opt out
- Remote-managed settings with an invalid entry now apply their remaining valid policies and surface the validation error, instead of silently dropping the whole payload
- Background sessions now preserve `--ide`, `--chrome`, `--bare`, `--remote-control`, and other flags across retire→wake, and respawn state validation was hardened
- Background sessions are now told that shared-checkout edits are blocked until they enter a worktree, avoiding a wasted rejected edit before `EnterWorktree`
- The "CLAUDE.md is too long" warning threshold now scales with the model's context window
- Auto-updater on Windows now stops retrying within a session once `claude.exe` is held by another process
- Improved color contrast for skill tags in the slash-command menu
- Promo credit claims for Apple/Google-billed subscribers without a payment method now explain where to add one
- Added a tip suggesting `claude agents` when running multiple concurrent sessions
## 2.1.168
- Bug fixes and reliability improvements
## 2.1.167
- Bug fixes and reliability improvements
## 2.1.166
- Added `fallbackModel` setting to configure up to three fallback models tried in order when the primary model is overloaded or unavailable; `--fallback-model` now also applies to interactive sessions
- Added glob pattern support in deny rule tool-name position (`"*"` denies all tools); allow rules reject non-MCP globs, and unknown tool names in deny rules warn at startup
- Hardened cross-session messaging: messages relayed via `SendMessage` from other Claude sessions no longer carry user authority — receivers refuse relayed permission requests, and auto mode blocks them
- `MAX_THINKING_TOKENS=0`, `--thinking disabled`, and the per-model thinking toggle now disable thinking on models that think by default via the Claude API (3P providers unchanged)
- Claude Code now retries a turn once on the fallback model when the API rejects an unexpected non-retryable error; auth, rate-limit, request-size, and transport errors still surface immediately
- `claude update` now announces the target version before downloading instead of going silent
- `claude agents`: typing a URL into the list now filters to the session whose first prompt contained it
- Fixed a recurring "image could not be processed" error and extra token usage when an unprocessable image was sent in a session
- Fixed remote sessions becoming permanently stuck when a brief backend disruption occurred during worker registration at startup
- Fixed flickering in JetBrains IDE terminals (IntelliJ, PyCharm, WebStorm, etc.) on 2026.1+ by enabling synchronized output
- Fixed Shift+non-ASCII characters (e.g. Shift+ä → Ä) being dropped in terminals using the Kitty keyboard protocol (WezTerm, Ghostty, kitty)
- Fixed PowerShell command validation occasionally hanging far past its time budget on Windows when a killed process's children held its output pipes
- Fixed orphaned `claude --bg-pty-host` processes spinning at 100% CPU after the daemon dies while connected on macOS
- Fixed voice mode requiring `/login` to clear a stale auth check after toggling `/voice`
- Fixed managed settings with an invalid entry silently disabling enforcement of their remaining valid policies
- Fixed managed-settings `allowedMcpServers`/`deniedMcpServers` predicates not matching when they use `${VAR}` references
- Fixed background agent sessions that entered a git worktree crash-looping with "No conversation found" when reopened from `claude agents`
- Fixed duplicated thinking text in the Ctrl+O transcript view while streaming
- Fixed `/doctor` showing a contradictory failed "Not inside a remote session" check when run inside a remote session
- Fixed the cursor sticking at the end of the first line when typing a multiline prompt in the `claude agents` dispatch and reply inputs
- Fixed blank lines appearing between background agent rows in the task list on terminals without Unicode support
## 2.1.165
- Bug fixes and reliability improvements
## 2.1.163
- Added `requiredMinimumVersion` and `requiredMaximumVersion` managed settings — Claude Code refuses to start if its version is outside the allowed range and directs the user to an approved version
- Added `/plugin list` command to list installed plugins, with `--enabled`/`--disabled` filters
- Added a "c to copy" shortcut to `/btw` that copies the raw markdown answer to the clipboard, preserving formatting when pasted elsewhere
- Hooks: Stop and SubagentStop hooks can now return `hookSpecificOutput.additionalContext` to give Claude feedback and keep the turn going without being labeled a hook error
- Skills: added `\$` escape syntax to include a literal `$` before a digit in command bodies
- stdio MCP servers now receive the same `CLAUDE_CODE_SESSION_ID` as hooks/Bash on `--resume`
- Fixed `claude -p` hanging forever after its final result when a backgrounded command never exits — background shells are now stopped ~5s after the result once stdin closes
- Fixed `claude -p` failing with "ANTHROPIC_API_KEY required" on Bedrock/Vertex/Foundry when `CI=true` and no Anthropic API key is set
- Fixed bash commands failing under bazel and EDR-protected Go workflows: `$TMPDIR` was overridden to `/tmp/claude-{uid}` for all commands instead of only sandboxed ones (regression in 2.1.154)
- Fixed Bash commands failing on Windows with "EEXIST: file already exists" on the session-env directory when it has the read-only attribute or is inside OneDrive
- Fixed org-managed permission rules not applying for the entire session when the managed settings fetch completed during startup on a fresh config directory
- Fixed background sessions in `claude agents` losing their running background tasks when reattached after a Claude Code update
- Fixed terminal misalignment and a multi-second hang when exiting the agent view by pressing Esc
- Fixed clicking Stop on a background-task chip in the desktop app not clearing the chip when the underlying process was already gone
- Fixed keyboard input becoming permanently unresponsive after a paste operation whose end marker is dropped by the terminal
- Fixed hook `if: "Bash(...)"` conditions firing on every Bash command containing `$()` or `$VAR`; the pattern now matches against commands inside subshells and backticks too
- Fixed deny rules on home-directory paths (e.g. `Read(~/Desktop/**)`) not blocking Bash commands that reference the path via `$HOME`
- Fixed a stray "(no content)" line left in the transcript after closing panel dialogs like /mcp and /plugins
- Background agent sessions now update to a new Claude Code version in the background, so opening a session after an update no longer waits on a cold restart
- Clearer descriptions for built-in commands and skills in the / menu
- The subscription-switch suggestion now shows in the startup announcement slot instead of a toast
- `claude agents` dispatching from the state-grouped view now starts the session in the directory the agent view was opened from
## 2.1.162
- `claude agents --json` now includes `waitingFor` showing what a waiting session is blocked on (e.g. permission prompt)
- `--tools`: explicitly listing Grep/Glob now provides the dedicated search tools on native builds with embedded search (previously these names were silently ignored)
- `/effort` now confirms when your chosen level will persist as the default for new sessions
- Clicking a slash command in the autocomplete menu now fills it into your prompt instead of running it immediately; press Enter to run
- Remote Control now shows as a persistent footer pill (with a link to the session) instead of a startup message
- Renamed Windsurf to Devin Desktop in the `/ide` menu, `/terminal-setup`, and `/scroll-speed`, following the editor's rebrand
- Fixed a silent startup hang when the config directory is read-only or unwritable — Claude Code now starts with in-memory config and surfaces startup errors instead of showing a blank screen
- Fixed WebFetch permission rules not being applied to built-in preapproved domains; explicit `WebFetch(domain:...)` deny/ask/allow rules now take precedence over the preapproved-host auto-allow
- Fixed Windows permission rules never matching when spelled with backslashes (`~\`, `\\server\share`) or case-variant paths, and Read deny rules not hiding files from Glob/Grep results
- Fixed an interrupt (Esc) sent at the very start of a turn being silently dropped in stream-json/SDK sessions, leaving the turn running with no "Interrupted" feedback
- Fixed API 400 `no low surrogate in string` errors for classifier side-queries and MCP server descriptions containing emoji near a truncation boundary
- Fixed MCP per-server `timeout` config values below 1000 ms being floored to a 1-second watchdog that aborted every tool call; sub-1000 ms values are now ignored (falling back to `MCP_TOOL_TIMEOUT` or default), and `claude mcp get` annotates them accordingly
- Fixed the LSP tool's `workspaceSymbol` operation returning no results; it now accepts a `query` parameter and passes it to the language server
- Fixed `claude agents` cutting live status text (tool args, replies, prompts, exec output) at 60120 columns on wide terminals; the status detail now uses the full terminal width
- Fixed `claude agents` truncating long session names at 40 columns; the name column now grows with terminal width
- Fixed `claude agents` attach occasionally bouncing straight back to the session list on the first try after a background-service restart
- Fixed `claude agents` Ctrl+V image paste doing nothing in the dispatch input and the session reply box; pasting with no image now shows a hint
- Fixed backgrounding a session with ← silently losing the conversation when the background service cannot start; the session stays in the list as a failed row you can wake with Enter
- Fixed replies from the agents view that fail to send being lost; they are now queued for delivery on the next session start
- Fixed cross-session messaging (`SendMessage`) silently breaking when `CLAUDE_CODE_TMPDIR` or `$TMPDIR` points at a deep directory
- Fixed opening a running background session from `claude agents` stalling for 5 seconds before attaching
- Quieter startup: notices group by severity, and session info and announcements share a single line per launch
- Startup warnings rewritten to be shorter and clearer, each with a concrete fix
- Launch-prompt warnings (deep link/pre-filled prompt) now stay pinned below the input until you act instead of scrolling away
- Failed turns now show a compact warning line instead of a multi-line red error block
- Improved background service startup and `claude update` verification to wait out endpoint-security scanning of new binaries instead of failing after 5 seconds
- Background dispatch spawn failures now report the error class name when no errno is available
- Removed the "Claude in Chrome enabled" and "marketplace installed" startup messages; model auto-updates and the team-onboarding tip now show as quiet notices under the logo
## 2.1.161
- `OTEL_RESOURCE_ATTRIBUTES` values are now included as labels on metric datapoints, so you can slice usage metrics by custom dimensions like team or repo
- `claude agents` rows now show `done/total` before the detail when work is fanned out; peek shows the longest-running item
- `/mcp` now collapses claude.ai connectors you've never signed in to behind a "Show unused connectors" row
- Parallel tool calls: a failed Bash command no longer cancels other calls in the same batch — each tool returns its own result independently
- Fullscreen mode: clipboard now uses `wl-copy`/`xclip`/`xsel` on Linux when available, copies to both the clipboard and PRIMARY selection for middle-click paste, and the "hold {key} for native selection" hint now shows the correct key per terminal
- Fixed the `/effort` dialog, workflow animations, and prompt keyword shimmer not honoring the "Reduce motion" setting
- Fixed `forceLoginOrgUUID`/`forceLoginMethod` managed-settings policies blocking third-party provider sessions (Bedrock, Vertex, Foundry, Mantle) alongside the org pin (regression in 2.1.146)
- Fixed background subagent output corrupting `claude -p` stdout when using `--output-format text` or `json`
- Fixed `/usage-credits` starting a re-login for Team and Enterprise admins instead of pointing to the organization's usage settings page
- Fixed `/autofix-pr` reporting "cannot run on the default branch" when the session is inside a git worktree or another repository
- Fixed `--resume` picker not showing sessions from the current directory when it isn't a git worktree (e.g., jj workspaces)
- Fixed Windows hooks that invoke bash explicitly (e.g., `/usr/bin/bash script.sh`) failing with "command not found" or "cannot execute binary file"
- Fixed OpenTelemetry log events (`user_prompt`, `api_request`, `tool_result`, `tool_decision`) being silently dropped when emitted before telemetry initialization completed
- Fixed `claude mcp` list/get/add printing secrets to the terminal: `${VAR}` references are no longer expanded, and credential headers and URL secrets are redacted
- Fixed Workflow agents spawned with `isolation: "worktree"` in background sessions being blocked from editing files inside their own worktree
- Fixed background sessions dispatched from `claude agents` booting on a stale model from the daemon's environment instead of the model in `settings.json`
- Fixed a potential crash when rendering Write tool results after resuming a session
- Fixed completed subagents getting stuck showing as running when an error occurs while finalizing their result
- Fixed `EADDRINUSE` errors from tools that bind Unix sockets under `$TMPDIR` when `CLAUDE_CODE_TMPDIR` is set to a deep path
- Improved terminal rendering performance by stabilizing the layout engine's JIT compilation profile
- Improved rendering performance for large file writes
- [VSCode] Added a tip suggesting disabling terminal GPU acceleration (or running `/terminal-setup`) to fix garbled glyphs
## 2.1.160
- Added a prompt before writing to shell startup files (`.zshenv`, `.zlogin`, `.bash_login`) and `~/.config/git/`, which could otherwise lead to unintended command execution
- `acceptEdits` mode now prompts before writing build-tool config files that grant code execution (`.npmrc`, `.yarnrc*`, `bunfig.toml`, `.bazelrc`, `.pre-commit-config.yaml`, `.devcontainer/`, etc.)
- Edit no longer requires a separate Read after viewing a file with `grep`: single-file `grep`/`egrep`/`fgrep` commands now satisfy the read-before-edit check
- Fixed copy-on-select not writing to the Windows clipboard on WSL — now uses PowerShell interop instead of OSC 52, which terminals like MobaXterm don't support
- Fixed restoring a completed session from `claude agents` dropping chat history and re-running the original prompt
- Fixed background sessions re-attached after overnight retire losing their conversation and re-running the original prompt
- Fixed `claude --bg` occasionally failing with "socket missing" when the background daemon was cold-starting on a loaded machine
- Fixed an issue on Windows where the directory a background session was started in could not be deleted after `claude rm` until the background daemon exited
- Fixed background agents that resumed work being shown under Completed in the agents list
- Fixed `claude agents` freezing for several seconds when returning to the session list due to the auto-updater re-checking on every exit
- Fixed Esc, arrow keys, and typing becoming unresponsive on Windows when attached to a background session or in the agent view while the host is under heavy CPU load
- Fixed background agents emitting terminal sync-output markers to terminals that don't support them (Apple Terminal, tmux), causing render artifacts when entering a running agent
- Fixed mouse wheel scrolling prompt history instead of the transcript right after opening a session from the agents list
- Fixed CJK IME composition appearing at the bottom-left of the screen instead of at the input caret in the `claude agents` view
- Fixed valid `file:///C:/...` links being rewritten to a broken path on Windows terminals with hyperlink support
- Fixed voice mode failing to connect when the project directory or branch name contains non-ASCII or special characters
- Fixed the auto mode unavailability message on third-party providers (Bedrock/Vertex/Foundry) to point to the `CLAUDE_CODE_ENABLE_AUTO_MODE` opt-in instead of incorrectly blaming the model
- Fixed `/effort ultracode` incorrectly blaming the dynamic workflows setting when the model cannot run xhigh; ultracode is no longer offered on models that do not support it
- Fixed model-not-found errors suggesting `--model` when running via the SDK or other hosts where the CLI flag doesn't apply
- Fixed Claude's past replies disappearing from scrollback when resuming a brief mode session with brief mode turned off
- Fixed vim mode `p` pasting on the line below instead of at the cursor when the register was yanked with `v$`
- Improved performance of opening recently-inactive background agent sessions in `claude agents`
- Improved auto mode classifier latency by reducing reasoning on routine actions, lowering the chance of "could not evaluate this action" blocks
- Improved background-session teardown (`claude rm`/`stop`, idle reap) to send SIGTERM to running shell subprocesses before SIGKILL, so cleanup handlers run
- Removed `CLAUDE_CODE_OPUS_4_6_FAST_MODE_OVERRIDE`; the environment variable is now a no-op
- Removed the JetBrains plugin install suggestion from startup
- Renamed the dynamic-workflow trigger keyword from `workflow` to `ultracode`. The word "workflow" no longer triggers a run; asking for one in your own words still works. The trigger keyword is highlighted in violet in the prompt input
## 2.1.159
- Internal infrastructure improvements (no user-facing changes)
## 2.1.158
- Auto mode is now available on Bedrock, Vertex, and Foundry for Opus 4.7 and Opus 4.8. Opt in by setting `CLAUDE_CODE_ENABLE_AUTO_MODE=1`
## 2.1.157
- Plugins in `.claude/skills` directories are now automatically loaded, no marketplace required
- Added `claude plugin init <name>` to scaffold a new plugin in `.claude/skills`
- Added autocomplete for `/plugin` arguments: subcommands, installed plugin names, and plugins from known marketplaces
- `claude agents`: the `agent` field in `settings.json` is now honored for dispatched sessions, with `--agent <name>` to override it
- `EnterWorktree` can now switch between Claude-managed worktrees mid-session
- `tool_decision` telemetry events now include `tool_parameters` (bash commands, MCP/skill names) when `OTEL_LOG_TOOL_DETAILS=1`
- Worktrees managed by Claude are now left unlocked when the agent finishes, so `git worktree remove`/`prune` can clean them up
- Fixed unprocessable images (zero-byte, corrupt) attached via paste, MCP, or dialog crashing the request instead of becoming a text placeholder
- Fixed sandbox network permission prompts appearing in auto and bypass-permissions mode when using the desktop app, IDE extensions, or SDK
- Fixed `claude agents` completed sessions not retiring when an idle subagent was still parked or had leaked a backgrounded shell
- Fixed `claude agents` pressing Esc not cancelling a slow "opening…", leaving the list unresponsive
- Fixed background agent worktrees under `.claude/worktrees/` being orphaned after the 30-day job retention sweep
- Fixed background sessions re-attached after a sleep/wake not telling the model the correct date
- Fixed copy-on-select in `claude agents` not reaching the system clipboard inside tmux with `set-clipboard on` (regression in 2.1.153)
- Fixed `--resume` not reporting background subagents that were running when the previous Claude Code process exited
- Fixed the `--resume` session picker leaving its contents on the terminal after exiting in fullscreen mode
- Fixed `--worktree` and `--worktree --tmux` returning to the canonical repo root instead of the current linked worktree
- Fixed the `/model` picker showing an incorrect "Newer version available" hint when the selected model is already the newest in its family; the pinned-model row now shows the model's description instead of its raw ID
- Fixed literal markdown markers (backticks, asterisks) appearing in the in-progress message text in fullscreen mode
- Fixed the terminal freezing after approving the managed-settings security dialog at startup
- Fixed a rare duplicate line appearing in scrollback after the terminal UI redraws
- Fixed right-click paste duplicating the clipboard in the VS Code, Cursor, and Windsurf integrated terminals
- WSL: fixed image paste (`alt+v` keybinding), screenshot paste on Windows 11, and added support for dragging images from Windows Explorer
- Improved performance of long and resumed conversations by eliminating redundant message-rendering recomputations
- `/terminal-setup` now disables GPU acceleration in VS Code/Cursor/Windsurf integrated terminals to prevent garbled-text rendering
- The Feature of the Week credit-claim status now appears as a notification in the status area instead of a line above the prompt
- `claude agents`: slash-command autocomplete in the dispatch input now matches substrings
- Removed the "bash commands will be sandboxed" startup banner — sandbox status still shows in `/status` and when a command is blocked
- Removed the "/ide for …" startup hint toast
- [IDE] Fixed clicking Stop while a background subagent is running not actually stopping it
- [VSCode] Fixed the fast mode indicator not appearing on Opus 4.8
- Pressing backspace right after a workflow trigger keyword now dismisses the workflow request (same as alt+w) instead of deleting a character
- Added a "Workflow keyword trigger" setting in /config to stop the word "workflow" in a prompt from triggering a dynamic workflow
## 2.1.156
- Fixed an issue when using Opus 4.8 where thinking blocks were modified, leading to API errors.
## 2.1.154
- Opus 4.8 is here! Now defaults to high effort · /effort xhigh for your hardest tasks
- Introducing dynamic workflows: ask Claude to create a workflow and it orchestrates work across tens to hundreds of agents in the background, so you can take on larger, more complex tasks. Run `/workflows` to view your runs
- Fast mode on Opus 4.8 is now available at a fraction of its previous cost: 2x the standard rate for 2.5x the speed
- The lean system prompt is now the default for all models except Haiku, Sonnet, and Opus 4.7 and earlier
- Claude now reserves the multiple-choice question prompt for decisions it genuinely cannot make itself, instead of asking when it already has enough context to proceed
- `/simplify` now runs a cleanup-only review (reuse, simplification, efficiency, altitude) and applies the fixes, instead of running the full `/code-review --fix` bug-hunting review
- Renamed the `/effort` slider labels from "Speed"/"Intelligence" to "Faster"/"Smarter" for clarity
- `claude agents`: type `! <command>` to run a shell command as a background session you can attach to and detach from. Also available as `claude --bg --exec '<command>'`
- `claude agents`: `/logout` now signs you out instead of being sent to a background session
- `←←` to open the agents view now works on Bedrock, Vertex, Foundry, and with telemetry disabled
- Claude in Chrome: pick which connected browser to use via `/chrome` → "Select browser…", or in-chat when a browser action runs with multiple connected
- Plugins can now declare `defaultEnabled: false` in `plugin.json` or a marketplace entry; enable them with `/plugin` or `claude plugin enable`. Dependencies of enabled plugins are still enabled automatically
- The `/plugin` Discover tab now pins plugins whose relevance signals match the current directory with a "suggested for this directory" annotation
- Streaming tool execution is now always enabled, including when telemetry is disabled or on Bedrock/Vertex/Foundry (previously behind a feature flag)
- Stdio MCP server subprocesses now receive `CLAUDE_CODE_SESSION_ID` and `CLAUDECODE=1` in their environment
- `claude mcp list`/`get` now show unapproved `.mcp.json` servers as `⏸ Pending approval` instead of auto-approving and connecting when output is piped
- `/remote-control` autocomplete now shows "Disconnect Remote Control" when Remote Control is already active
- Added Claude Opus 4.8 support and 4.7 → 4.8 migration guidance to the `/claude-api` skill
- Deprecated `CLAUDE_CODE_OPUS_4_6_FAST_MODE_OVERRIDE` (will be removed on 06/01). To use fast mode on Opus 4.6, switch with `/model claude-opus-4-6[1m]` and then `/fast on`
- Improved the auto-mode classifier's detection of data exfiltration, particularly bulk transfers of repository contents
- Fixed `rm -rf $HOME` not being blocked as a dangerous path when `HOME` has a trailing slash
- Fixed `$TMPDIR` resolving to different directories in sandboxed vs unsandboxed Bash commands within the same session
- Fixed unreadable highlighted-row text in `claude agents` when the Claude Code theme doesn't match the terminal background
- Fixed background-agent completion notifications triggering premature "out of context" behavior on some 1M-context models
- Fixed background-session classifier losing the user's goal when a scheduled `/command` fires
- Fixed pinned background sessions respawning every minute after a Claude Code update, causing repeated agent-start notifications and process churn at idle
- Fixed background sessions stuck at "blocked", "running", or "working" not retiring after the idle grace period
- Fixed subagents in background sessions bypassing the worktree-isolation guard and writing to the shared checkout
- Fixed orphaned `claude --bg-pty-host` processes spinning at 100% CPU after the daemon exits on macOS
- Fixed number key shortcuts not working for options shown below the divider in option dialogs
- Fixed `worktree.baseRef: "head"` resolving to the main checkout's HEAD instead of the current worktree's HEAD when spawning subagents or calling `EnterWorktree` from inside a linked worktree
- Fixed a stray leading space on wrapped lines when the previous line ended exactly at the terminal width
- Fixed intermittent terminal rendering corruption in VS Code by capping the number of distinct colors the thinking spinner produces
- Fixed plan file names including `[Image #N]` / `[Pasted text #N]` placeholders when a plan-mode prompt starts with pasted images or text
- Fixed a phantom expand/click affordance on colored tool output: short ANSI-colored lines that fit on screen no longer show a "ctrl+o to expand" hint
- Fixed a single invalid `allowedMcpServers`/`deniedMcpServers` entry in managed settings discarding all managed-settings policy; the bad entry is now dropped with a `claude doctor` warning
- Fixed API 400 errors on models that don't support the effort parameter when `CLAUDE_CODE_ALWAYS_ENABLE_EFFORT` is set
- Windows: Fixed update failures caused by `claude.exe` being in use showing a generic error instead of telling you to close other sessions and retry
- Removed the stale "& for background" hint from the shortcuts help panel
- [VSCode] Auto mode no longer requires the bypass-permissions setting to appear in the mode picker, and a dismissable notice on the new-session screen explains auto mode the first time it's active
- Fixed the task panel below the prompt showing a stray unselectable "main" row when only a workflow is running
- Fixed /mcp tools list and tool detail rendering when MCP servers have long or multi-line tool names or long descriptions
- Fixed the /model picker not showing fast mode pricing on the Default option for API (pay-as-you-go) users when fast mode is on
- Fixed auto mode incorrectly blocking actions with "could not evaluate this action" when the safety classifier ran out of output tokens while reasoning
## 2.1.153
- Added `skipLfs` option to `github`/`git` plugin marketplace sources to skip Git LFS downloads during clone and update

View File

@@ -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

12
examples/gateway/gcp/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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"]

View File

@@ -0,0 +1,18 @@
# Claude Gateway on Google Cloud
Reference deployment artifacts for running Claude Gateway on GCP with Vertex AI
as the upstream: Cloud Run or GKE, Cloud SQL for PostgreSQL, Secret Manager, and
service-account auth to Vertex AI.
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
- **Public mirror**: https://github.com/anthropics/claude-code/tree/main/examples/gateway/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 (Vertex upstream, Google Workspace IdP) |
| `terraform/` | Provisions the full architecture (two-pass apply — see `terraform/README.md`) |

View File

@@ -0,0 +1,155 @@
# gateway.yaml.example — Claude Gateway config template, GCP-shaped (walkthrough §6).
#
# Google Workspace IdP + Vertex 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 — Vertex AI ────────────────────────────────────────────────────
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 Vertex 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 }

558
examples/gateway/gcp/setup.sh Executable file
View File

@@ -0,0 +1,558 @@
#!/usr/bin/env bash
#
# setup.sh — GCP setup for Claude Gateway (walkthrough §17b).
#
# 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 (Vertex 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 # Vertex 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 (Vertex, 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

View File

@@ -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

View File

@@ -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) §17): 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).

View File

@@ -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" # Vertex 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 (Vertex, 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"
}

View File

@@ -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
}

View File

@@ -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-..."

View File

@@ -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. (Vertex 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
}

View File

@@ -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
}

966
feed.xml

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "frontend-design",
"version": "1.0.0",
"version": "1.1.0",
"description": "Frontend design skill for UI/UX implementation",
"author": {
"name": "Prithvi Rajasekaran, Alexander Bricken",

View File

@@ -1,42 +1,55 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
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
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
# Frontend Design
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
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.
## Design Thinking
## Ground it in the subject
Before coding, understand the context and commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
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.
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
## Design principles
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
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.
## Frontend Aesthetics Guidelines
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.
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; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
- **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. Use scroll-triggering and hover states that surprise.
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
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.
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
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.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
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.
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the 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.
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
## 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 46 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.