mirror of
https://github.com/anthropics/claude-code.git
synced 2026-07-04 07:53:29 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1322e9bacc | ||
|
|
125d63feae | ||
|
|
5dc12eb281 | ||
|
|
75709eacf1 | ||
|
|
a56ff02e85 | ||
|
|
c80896ca84 | ||
|
|
3c3558207e | ||
|
|
f605f0b68d | ||
|
|
27e561ba3d | ||
|
|
6234fa8f14 |
117
CHANGELOG.md
117
CHANGELOG.md
@@ -1,5 +1,122 @@
|
||||
# Changelog
|
||||
|
||||
## 2.1.200
|
||||
|
||||
- Changed `AskUserQuestion` dialogs to no longer auto-continue by default; opt into an idle timeout via `/config`
|
||||
- Changed the "default" permission mode to "Manual" across the CLI, `--help`, VS Code, and JetBrains; `--permission-mode manual` and `"defaultMode": "manual"` are accepted alongside `default`
|
||||
- Fixed a crash at startup when `disabledMcpServers` or `enabledMcpServers` in `.claude.json` is set to a non-array value
|
||||
- Fixed background sessions silently stopping mid-turn after sleep/wake or when reopening a stalled session
|
||||
- Fixed background sessions re-running a turn cancelled with Esc after a stall respawn
|
||||
- Fixed background agents never starting again after a crash left a stale `daemon.lock` whose PID the OS reused
|
||||
- Fixed background-agent daemon handover so a reinstalled older build can no longer take over the daemon; build recency is now judged by the version's embedded build timestamp
|
||||
- Fixed background-agent roster issues: transient corruption permanently disabling orphan cleanup, older binaries not preserving fields written by newer versions, and socket auth tokens being stripped during daemon restarts
|
||||
- Fixed subagents cut off by a rate limit before producing any text output returning an empty result instead of failing cleanly
|
||||
- Fixed control bytes from background-agent output reaching the terminal in the agent view
|
||||
- Fixed `claude agents --plugin-dir <dir>` not showing the plugin's agents and skills in the agent view when the flag is placed after `agents`
|
||||
- Fixed project-scoped plugins not loading correctly from git worktrees of the same repository
|
||||
- Fixed `/mcp` server list not tracking focus for screen readers and magnifiers
|
||||
- Fixed voice dictation showing a misleading "Voice connection failed" message when a recording captures no audio
|
||||
- Fixed rendering flicker under tmux 3.4+ by enabling synchronized terminal output
|
||||
- Improved screen-reader output: decorative glyphs are now hidden, transcript symbols read as short labels, and nested tables read as `Header: value.` lines
|
||||
- Improved the install script to explain when installation is killed by the system running out of memory
|
||||
|
||||
## 2.1.199
|
||||
|
||||
- Stacked slash-skill invocations like `/skill-a /skill-b do XYZ` now load all leading skills (up to 5), not just the first
|
||||
- Fixed SSL certificate errors (TLS-inspecting proxies, missing `NODE_EXTRA_CA_CERTS`, expired certs) burning retries before showing actionable guidance — they now fail immediately with the fix hint
|
||||
- Fixed streaming responses being discarded when the API emits a mid-stream overloaded/server error after partial output — the partial is now kept with an incomplete-response notice
|
||||
- Fixed subagents cut off by a rate limit or server error silently failing instead of returning their partial work to the parent
|
||||
- Fixed subagents reporting API errors (e.g. usage limit reached) as successful results — the error is now reported to the parent agent
|
||||
- Fixed the background-agent daemon on Linux killing itself and every running agent every ~50 seconds after an unclean shutdown left a corrupted worker record
|
||||
- Fixed background agents failing to cold-start over SSH on macOS with "Could not switch to audit session" (regression in 2.1.196)
|
||||
- Fixed `claude stop` being silently undone when it raced a background-agent respawn — the respawn now honors the stop
|
||||
- Fixed background job progress indicators stalling for minutes while the job ran long commands
|
||||
- Fixed background sessions on memory-starved machines showing a generic error — they now indicate low memory and suggest freeing resources
|
||||
- Fixed remote sessions briefly flapping between Working and Idle in the agent view when a background agent completes
|
||||
- Fixed idle subagents vanishing from the agent panel while other subagents were still working; surplus idle agents now collapse into an expandable summary row
|
||||
- Fixed typing `/model` or `/fast` while viewing a subagent silently opening the lead's model picker — a notice now explains the command applies to the lead
|
||||
- Fixed `SessionStart`, `Setup`, and `SubagentStart` hooks silently hiding stderr when exiting with code 2 — the error is now shown in the transcript
|
||||
- Fixed `claude --dangerously-skip-permissions daemon <subcommand>` being treated as a chat prompt instead of running the subcommand
|
||||
- Fixed `SendMessage` silently misrouting when a re-spawned agent reuses a previous agent's name — the tool now detects the mismatch and asks the caller to retarget
|
||||
- Fixed opening or resuming a session with no new messages needlessly growing the transcript file
|
||||
- Fixed backgrounding a session with `←` or `/background` dropping its `/color` from the agent view row
|
||||
- Fixed resetting a corrupted config file from the startup recovery dialog destroying it unrecoverably — it now backs up the file first
|
||||
- Fixed Claude in Chrome repeatedly opening the reconnect page when sessions run from different builds or config directories
|
||||
- Fixed plan mode not prompting for state-changing browser tool calls; read-only `browser_batch` calls are now correctly auto-allowed
|
||||
- Transient server rate-limit errors (429s unrelated to your usage limit) are now retried automatically with backoff for subscribers instead of failing the turn
|
||||
- `CLAUDE_CODE_RETRY_WATCHDOG` now raises the default retry count for non-capacity transient errors to 300 and lifts the cap of 15 on `CLAUDE_CODE_MAX_RETRIES`
|
||||
- `claude agents` session rows now show pull-request links as bare `#N` without the redundant "PR" label
|
||||
|
||||
## 2.1.198
|
||||
|
||||
- Subagents now run in the background by default, so Claude keeps working while they run and is notified when they finish (previously a gradual rollout)
|
||||
- Claude in Chrome is now generally available
|
||||
- Added background agent notifications in `claude agents` — sessions that need input or finish now fire the `Notification` hook (`agent_needs_input` / `agent_completed`)
|
||||
- Added `/dataviz` skill for chart and dashboard design guidance with a runnable color-palette validator
|
||||
- Gateway: added Claude Platform on AWS (anthropicAws) as an upstream provider; model-not-found responses now advance the failover chain
|
||||
- Background agents launched from `claude agents` now commit, push, and open a draft PR when they finish code work in a worktree, instead of stopping to ask
|
||||
- The built-in Explore agent now inherits the main session's model (capped at opus) instead of running on haiku
|
||||
- Subagents and context compaction now inherit the session's extended thinking configuration, improving output quality on delegated tasks
|
||||
- Fixed brief network drops mid-response aborting the turn — transient errors like ECONNRESET now retry with backoff instead of failing
|
||||
- Fixed excessive background classifier requests when sandboxed processes repeatedly accessed the same network host
|
||||
- Fixed background tasks in web, desktop, and VS Code task panels getting stuck on "Running" after they finish or after resuming a session
|
||||
- Fixed agent teams: a teammate that dies on an API error now reports "failed" to the lead, and messaging a stuck teammate wakes it to retry immediately
|
||||
- Fixed the `/diff` panel not refreshing when you switch branches or commit outside the session
|
||||
- Fixed markdown tables overflowing and wrapping their right border when rendered in fullscreen mode
|
||||
- Fixed Claude Platform on AWS and Mantle sessions dead-ending with "Please run /login" when the STS token expires — `awsAuthRefresh` now runs automatically
|
||||
- Fixed "no route to host" for local-network hosts in macOS background agent sessions by declaring Local Network entitlements
|
||||
- Fixed `/desktop` failing with "Cannot determine working directory" after entering and exiting a worktree
|
||||
- Fixed background agents repeatedly showing "Reconnecting…" every ~52 seconds on macOS while the agents view was open
|
||||
- Fixed pressing `←` inside `claude attach <id>` exiting to the shell instead of opening the agent view
|
||||
- Fixed `claude --bg` silently creating an unattachable session when combined with `--print`/`-p`; the conflicting flags are now rejected up front
|
||||
- Fixed the workflow progress view dropping the earliest agents from the list while the phase counter stayed correct in SDK and desktop-app sessions
|
||||
- Fixed `.claude/rules/` conditional rules not loading when the target file is reached via a symlinked path
|
||||
- Fixed Cmd+click not opening URLs in fullscreen mode in Warp on macOS
|
||||
- Fixed double-click word selection in fullscreen mode to select the entire URL including the scheme
|
||||
- Fixed plan mode not auto-allowing read-only tool calls when a session starts in plan mode
|
||||
- Fixed `/branch` deriving its default fork name from the compaction summary instead of the first real prompt
|
||||
- Improved focus mode: subagents launched in a turn now appear in its activity summary, and completed background notifications fold into a single count
|
||||
- Improved syntax highlighting accuracy in code blocks, diffs, and file previews by upgrading to highlight.js 11
|
||||
- Keyboard shortcut hints now show opt/cmd instead of alt/super when connected from a Mac over SSH
|
||||
- Improved API retry UX: the error reason is now shown after the second attempt, and a status page link replaces the spinner tip when the API is overloaded
|
||||
- `/login` now opens the sign-in dialog from the `claude agents` view instead of saying it isn't available
|
||||
- Subagents now treat messages from the agent that launched them as normal task direction; an agent's message is still never treated as the user's approval
|
||||
- Removed the `/agents` wizard; ask Claude to create or manage subagents, or edit `.claude/agents/` directly
|
||||
|
||||
## 2.1.197
|
||||
|
||||
- Introducing Claude Sonnet 5: now the default model in Claude Code, with a native 1M-token context window and promotional pricing of $2/$10 per Mtok through August 31. Update to version 2.1.197 for access. https://www.anthropic.com/news/claude-sonnet-5
|
||||
|
||||
## 2.1.196
|
||||
|
||||
- Added support for organization default models — admins set it in the org console; it shows as "Org default" (or "Role default") in `/model` when you haven't picked one yourself
|
||||
- Added readable default names for sessions at start, making them easier to identify and message
|
||||
- Added clickable file attachments in chat — Cmd/Ctrl-click reveals the file in Finder/Explorer
|
||||
- Security: `claude mcp list`/`get` no longer spawn `.mcp.json` servers that a repo self-approved via a committed `.claude/settings.json`; untrusted workspaces show `⏸ Pending approval`
|
||||
- Fixed waking a background job permanently deleting its conversation and re-running the original prompt when the transcript probe misread a real transcript; the file is now set aside, never deleted
|
||||
- Fixed the rate-limit warning flickering off and rate-limit telemetry being over-counted when multiple parallel requests were in flight at the moment a usage limit was hit
|
||||
- Fixed duplicate recap lines after a background session's turn: a schema-rejected StructuredOutput attempt no longer renders alongside its retry
|
||||
- Fixed PowerShell `git diff`/`git grep`, `egrep`/`fgrep`, and quoted search patterns containing `|` being reported as failures when they exit 1, matching Bash behavior
|
||||
- Fixed multiple `claude agents` side panel issues: keyboard focus getting stuck when opening an agent, background jobs losing their subagent types on every open, and sessions showing incorrect status while actively running
|
||||
- Fixed `claude agents --dangerously-skip-permissions` silently falling back to auto mode instead of showing the bypass disclaimer and applying bypass mode to spawned agents
|
||||
- Fixed mid-turn crash recovery for Remote sessions — sessions interrupted by a server restart now auto-resume on the next worker
|
||||
- Fixed sessions moved with `/cd` reappearing in the old directory's resume list after a non-graceful exit when the old path contained special characters
|
||||
- Fixed `claude plugin validate` skipping local plugins whose source is "." and stopping after the first error class
|
||||
- Fixed Esc Esc at an idle prompt not opening the rewind menu (regression); use Ctrl+C or Ctrl+X Ctrl+K to stop background agents
|
||||
- Fixed MCP OAuth requesting the authorization server's full `scopes_supported` catalog when no scope is specified, causing `invalid_scope` failures on GitLab self-hosted and other enterprise IdPs
|
||||
- Fixed `/context` showing 0 tokens for all tool groups on Bedrock
|
||||
- Fixed `/deep-research` misreporting verifier failures as "all claims refuted" instead of `unverified`
|
||||
- Fixed plugin dependency version pins not being honored when the marketplace was added as a local folder path backed by a git repo
|
||||
- Fixed `claude agents` session status: completed rows no longer flip between "Done" and "Needs your input", stalled agents are now labeled "Needs attention", and results that mention a PR show a clickable link
|
||||
- Fixed voice dictation swallowing spaces and spuriously starting a recording during very fast typing when voice mode is enabled
|
||||
- Improved background session reliability: long-running commands and workflows now survive the session's process being stopped, restarted, or updated — including on Windows, where background shells are handed off instead of being killed
|
||||
- Improved background agents: workers killed by a daemon restart are now automatically resumed from where they left off the next time the agents view opens
|
||||
- Improved `/code-review` workflow: merged five cleanup finders into one, cutting token usage by roughly 25%
|
||||
- Reduced per-frame rendering work in the terminal UI by skipping no-op subtree walks during streaming
|
||||
- The streaming idle watchdog is now on by default for all providers — it aborts and retries when a response stream produces no events for 5 minutes. Set `CLAUDE_ENABLE_STREAM_WATCHDOG=0` to disable.
|
||||
- Remote Control is now disabled when `ANTHROPIC_BASE_URL` points at a non-Anthropic host, matching the existing behavior under `CLAUDE_CODE_USE_BEDROCK`/`_VERTEX`/`_FOUNDRY`
|
||||
- Changed opening the agents view from a foreground session to require a single `←` press instead of two, matching the behavior in background sessions
|
||||
|
||||
## 2.1.195
|
||||
|
||||
- Added `CLAUDE_CODE_DISABLE_MOUSE_CLICKS` to disable mouse click/drag/hover in fullscreen mode while keeping wheel scroll
|
||||
|
||||
13
examples/gateway/gcp/.dockerignore
Normal file
13
examples/gateway/gcp/.dockerignore
Normal 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
12
examples/gateway/gcp/.gitignore
vendored
Normal 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
|
||||
35
examples/gateway/gcp/Dockerfile
Normal file
35
examples/gateway/gcp/Dockerfile
Normal 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"]
|
||||
17
examples/gateway/gcp/README.md
Normal file
17
examples/gateway/gcp/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Claude Gateway on Google Cloud
|
||||
|
||||
Reference deployment artifacts for running Claude Gateway on GCP with Agent
|
||||
Platform (formerly Vertex AI) as the upstream: Cloud Run or GKE, Cloud SQL for
|
||||
PostgreSQL, Secret Manager, and service-account auth to Agent Platform.
|
||||
|
||||
These files are provided as a working example rather than a supported production
|
||||
deployment. Adapt them to your own environment.
|
||||
|
||||
- **Walkthrough**: https://code.claude.com/docs/en/claude-apps-gateway-on-gcp
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `setup.sh` | Scripts the walkthrough end to end via `gcloud` |
|
||||
| `Dockerfile` | Runtime image for the `claude gateway` binary |
|
||||
| `gateway.yaml.example` | Gateway config template, GCP-shaped (Agent Platform upstream, Google Workspace IdP) |
|
||||
| `terraform/` | Provisions the full architecture (two-pass apply — see `terraform/README.md`) |
|
||||
156
examples/gateway/gcp/gateway.yaml.example
Normal file
156
examples/gateway/gcp/gateway.yaml.example
Normal file
@@ -0,0 +1,156 @@
|
||||
# gateway.yaml.example — Claude Gateway config template, GCP-shaped (walkthrough §6).
|
||||
#
|
||||
# Google Workspace IdP + Agent Platform (formerly Vertex AI) upstream, following
|
||||
# the walkthrough at https://code.claude.com/docs/en/claude-apps-gateway-on-gcp.
|
||||
# The active sections
|
||||
# below are a strict subset of the full configuration reference at
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway; optional keys are included
|
||||
# commented-out.
|
||||
#
|
||||
# USAGE — this is the shippable TEMPLATE. Copy it to gateway.yaml and fill it in:
|
||||
# cp gateway.yaml.example gateway.yaml
|
||||
# setup.sh and terraform/ read gateway.yaml (your filled-in copy, which is
|
||||
# gitignored). It is published as the Secret Manager secret `gateway-config`
|
||||
# (§6) and mounted at /etc/claude/gateway.yaml — the container ENTRYPOINT runs
|
||||
# `claude gateway --config /etc/claude/gateway.yaml`.
|
||||
#
|
||||
# Secret expansion: ${ENV_VAR} reads an env var; ${file:/path} reads a mounted file.
|
||||
# On Cloud Run, setup.sh injects the JWT / OIDC / Postgres secrets as ENV VARS
|
||||
# (Cloud Run can't mount multiple secrets into a single directory), and mounts
|
||||
# only gateway.yaml itself as a file at /etc/claude. On GKE you may use file mounts.
|
||||
#
|
||||
# BEFORE DEPLOY — replace every REPLACE_ME placeholder below (setup.sh refuses to
|
||||
# publish the config secret while any remain), and create the referenced secrets:
|
||||
# gateway-jwt-secret (setup.sh generates this)
|
||||
# gateway-oidc-client-secret (from the Google Cloud Console OAuth client)
|
||||
# gateway-postgres-url (setup.sh generates this)
|
||||
|
||||
# ── Listener ─────────────────────────────────────────────────────────────────
|
||||
listen:
|
||||
host: 0.0.0.0
|
||||
port: 8080 # Cloud Run sets PORT=8080; leave as-is
|
||||
# Required. Fixes the IdP redirect_uri, the OIDC discovery doc, and the
|
||||
# gateway-token issuer so none are derived from the client-controlled Host
|
||||
# header (X-Forwarded-Host/-Proto are likewise never trusted). On Cloud Run
|
||||
# the run.app URL is only assigned on the first deploy, so this starts as a
|
||||
# placeholder for the provisioning-only first pass (login does NOT work until
|
||||
# the real URL is set). After the first deploy, setup.sh prints the run.app
|
||||
# URL: set it here (or your LB hostname) and re-run; setup.sh republishes the
|
||||
# config and redeploys. Register the same host's /oauth/callback on the
|
||||
# Google OAuth client.
|
||||
public_url: https://set-after-first-deploy.invalid
|
||||
# Register this exact redirect URI on the Google OAuth client:
|
||||
# https://<public_url host>/oauth/callback
|
||||
#
|
||||
# On Cloud Run (or behind any L7 LB) every request arrives via Google's front
|
||||
# end, so the gateway sees one peer IP for all developers — set trusted_proxies
|
||||
# so X-Forwarded-For from those proxies is trusted and per-IP rate limiting /
|
||||
# audit IPs record the real client. 169.254.0.0/16 is Cloud Run's fixed
|
||||
# link-local serving range; the proxy-only subnet is the one your internal ALB
|
||||
# uses in this VPC.
|
||||
# trusted_proxies:
|
||||
# - 169.254.0.0/16 # Cloud Run serving proxy (link-local peer)
|
||||
# - <proxy-only-subnet-cidr> # add if fronted by your internal ALB (its proxy-only subnet)
|
||||
#
|
||||
# Alternative — terminate TLS in the gateway itself instead of at a proxy:
|
||||
# tls:
|
||||
# cert: /certs/gateway.crt
|
||||
# key: /certs/gateway.key
|
||||
|
||||
# ── Identity provider — Google Workspace ─────────────────────────────────────
|
||||
oidc:
|
||||
issuer: https://accounts.google.com
|
||||
client_id: REPLACE_ME # Google OAuth client ID (not secret; from Cloud Console)
|
||||
client_secret: ${OIDC_CLIENT_SECRET}
|
||||
allowed_email_domains: [REPLACE_ME] # e.g. [example.com] — reject id_tokens outside your org
|
||||
# Google ignores the default offline_access scope; these two are what actually
|
||||
# yield refresh tokens (silent renewal + the deprovision leash) from Google.
|
||||
scopes: [openid, profile, email]
|
||||
extra_auth_params: { access_type: offline, prompt: consent }
|
||||
# NOTE: Google id_tokens carry NO groups claim. For group-based RBAC with
|
||||
# Google as IdP, set `google_groups` (below) and the gateway fetches each
|
||||
# user's Workspace groups at login via the Admin SDK Directory API.
|
||||
# Otherwise, use email_domain matching (see managed.policies below).
|
||||
# google_groups:
|
||||
# service_account_json_path: /secrets/google-sa.json # SA with domain-wide delegation on admin.directory.group.readonly
|
||||
# admin_email: admin@example.com # a Workspace admin the SA impersonates
|
||||
# groups_claim: groups # Okta=groups, Entra app roles=roles — NOT Google
|
||||
# ca_cert_pem: ${file:/secrets/idp-ca.pem} # only for an IdP behind a private CA
|
||||
|
||||
# ── Sessions ─────────────────────────────────────────────────────────────────
|
||||
session:
|
||||
jwt_secret: ${GATEWAY_JWT_SECRET} # >= 32 bytes; openssl rand -base64 32
|
||||
# Google issues refresh tokens (above), so sessions renew silently and this
|
||||
# mainly bounds deprovision latency. 8 is a sane default; lower toward 1 for
|
||||
# tighter revocation. Array form rotates keys: [new, old] (index 0 signs, all verify).
|
||||
ttl_hours: 8
|
||||
|
||||
# ── Store (REQUIRED — the gateway refuses to boot without it) ─────────────────
|
||||
store:
|
||||
postgres_url: ${GATEWAY_POSTGRES_URL} # private-IP Cloud SQL; built with ?sslmode=require by setup.sh
|
||||
|
||||
# ── Upstreams — Agent Platform ───────────────────────────────────────────────
|
||||
upstreams:
|
||||
- provider: vertex
|
||||
region: us-east5 # a region where the Claude models you need are published in Model Garden
|
||||
project_id: REPLACE_ME # your GCP project ID for Agent Platform access
|
||||
auth: {} # ADC via Cloud Run SA / GKE Workload Identity (preferred — no static keys)
|
||||
# base_url: https://us-east5-aiplatform.p.googleapis.com # Private Service Connect endpoint
|
||||
# Add more upstreams for failover (tried top→bottom on 5xx/timeout/501): a
|
||||
# second region, or an anthropic/bedrock fallback. See
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway.
|
||||
|
||||
# ── Telemetry fan-out (OPTIONAL) ─────────────────────────────────────────────
|
||||
# The CLI sends OTLP/HTTP to the gateway; the gateway fans out, stamping
|
||||
# user.id/user.email/user.groups server-side. On GCP, point at an OpenTelemetry
|
||||
# Collector with the googlecloud exporter (-> Cloud Trace / Managed Prometheus).
|
||||
# Takes effect after the second pass (once public_url is the real URL, not the
|
||||
# placeholder): when forward_to and public_url are both configured the gateway pushes
|
||||
# CLAUDE_CODE_ENABLE_TELEMETRY and the OTEL exporter selectors to every client
|
||||
# automatically — no per-developer config needed.
|
||||
# telemetry:
|
||||
# forward_to:
|
||||
# - url: https://otel-collector.internal.example.com:4318
|
||||
# headers:
|
||||
# Authorization: ${file:/secrets/otlp-token}
|
||||
# metrics: true # safe aggregate counters (default)
|
||||
# logs: false # carries bash commands / tool inputs — opt in deliberately
|
||||
# traces: false
|
||||
|
||||
# ── RBAC + managed settings (OPTIONAL; first-match-wins, top -> bottom) ───────
|
||||
# With Google as IdP, match on email_domain, or on group email addresses
|
||||
# (e.g. eng@example.com) once oidc.google_groups is configured above.
|
||||
# managed:
|
||||
# policies:
|
||||
# - match: { email_domain: example.com }
|
||||
# cli:
|
||||
# availableModels: [claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5]
|
||||
# permissions: { deny: ["Read(./.env)", "Read(./secrets/**)"] }
|
||||
# - match: {} # catch-all floor — keep LAST
|
||||
# cli:
|
||||
# availableModels: [claude-sonnet-4-6, claude-haiku-4-5]
|
||||
|
||||
# ── Admin API (OPTIONAL — enables db-mode runtime config + spend caps) ───────
|
||||
# admin_groups needs a groups claim — with Google as IdP, set
|
||||
# oidc.google_groups (above) so Workspace group email addresses populate the
|
||||
# claim, or use the bootstrap keys below instead. Named keys for
|
||||
# attribution in the audit log; 32-char minimum on key values. On Cloud Run add
|
||||
# these as env vars to --set-secrets (or terraform env value_source blocks),
|
||||
# same as the JWT/OIDC/Postgres secrets above; on GKE you may use ${file:...}.
|
||||
# admin:
|
||||
# write_keys:
|
||||
# - id: terraform
|
||||
# key: ${GATEWAY_ADMIN_WRITE_KEY}
|
||||
# read_keys:
|
||||
# - id: reporting
|
||||
# key: ${GATEWAY_ADMIN_READ_KEY}
|
||||
# # admin_groups: [platform-finops@example.com] # group emails via oidc.google_groups, or any groups-capable IdP
|
||||
|
||||
# ── Model catalog (OPTIONAL) ─────────────────────────────────────────────────
|
||||
# Default true: every built-in Claude model is exposed and auto-translated per
|
||||
# upstream. Set false + a models: list to pin IDs (e.g. provisioned throughput).
|
||||
# auto_include_builtin_models: true
|
||||
# models:
|
||||
# - id: claude-opus-4-8
|
||||
# label: Claude Opus 4.8
|
||||
# upstream_model: { vertex: claude-opus-4-8 }
|
||||
558
examples/gateway/gcp/setup.sh
Executable file
558
examples/gateway/gcp/setup.sh
Executable file
@@ -0,0 +1,558 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# setup.sh — GCP setup for Claude Gateway (walkthrough §1–7b).
|
||||
#
|
||||
# Provisions, in doc order: APIs (§1), service account + IAM (§2), the gateway
|
||||
# container image in Artifact Registry (§3), a Cloud SQL (PostgreSQL) backend
|
||||
# with PRIVATE IP only (§4), the JWT + postgres-url secrets (§5), the
|
||||
# gateway.yaml config secret (§6), and a Cloud Run deploy with Direct VPC
|
||||
# egress (§7b).
|
||||
#
|
||||
# Private IP is required because public IP is disallowed by the org-policy constraint
|
||||
# `constraints/sql.restrictPublicIp`. A Cloud SQL private IP is an address inside a VPC,
|
||||
# so §4 here also provisions the prerequisite VPC + Private Services Access — the
|
||||
# one-time, irreducible networking required for private IP.
|
||||
#
|
||||
# Section markers (§N) below map to the walkthrough:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-on-gcp
|
||||
#
|
||||
# Covers here: APIs (§1) -> service account + IAM (§2) -> build & push image (§3)
|
||||
# -> VPC + Private Services Access -> Cloud SQL (private IP only) -> database
|
||||
# + user (§4) -> jwt + postgres-url secrets (§5) -> gateway-config
|
||||
# secret from gateway.yaml (§6) -> Cloud Run deploy (§7b).
|
||||
# Not covered: GKE track (§7a) — Cloud Run is the lower-friction path here.
|
||||
#
|
||||
# Idempotent: existing resources are detected and skipped, so it is safe to re-run.
|
||||
# Override any default below via environment variable, e.g. `REGION=us-east5 ./setup.sh`.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- configuration (env-overridable) ----------------------------------------
|
||||
PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project 2>/dev/null)}"
|
||||
REGION="${REGION:-${CLOUDSDK_COMPUTE_REGION:-us-east5}}" # guide §1 uses us-east5 (Agent Platform model region)
|
||||
|
||||
SA_NAME="${SA_NAME:-claude-gateway}" # §2 service account
|
||||
SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
|
||||
|
||||
# §3 image
|
||||
AR_REPO="${AR_REPO:-claude-gateway}" # Artifact Registry repository
|
||||
IMAGE_NAME="${IMAGE_NAME:-gateway}"
|
||||
RELEASES_URL="${RELEASES_URL:-https://downloads.claude.ai/claude-code-releases}" # public Claude Code release endpoint
|
||||
VERSION="${VERSION:-}" # Claude Code release to deploy; empty = latest release (resolved below)
|
||||
VERSION_FILE="${VERSION_FILE:-./.claude-version}" # pins the resolved release across re-runs; delete it (or set VERSION) to upgrade
|
||||
DOCKERFILE="${DOCKERFILE:-./Dockerfile}"
|
||||
CLAUDE_BINARY="${CLAUDE_BINARY:-./claude}" # linux-x64 Claude Code binary; downloaded from RELEASES_URL if missing
|
||||
CLAUDE_SHA256="${CLAUDE_SHA256:-}" # optional: out-of-band sha256 pin for the downloaded binary, checked in addition to the release manifest
|
||||
|
||||
VPC_NETWORK="${VPC_NETWORK:-cc-gateway-vpc}"
|
||||
SUBNET="${SUBNET:-cc-gateway-subnet}"
|
||||
SUBNET_RANGE="${SUBNET_RANGE:-10.0.0.0/24}"
|
||||
|
||||
PSA_RANGE_NAME="${PSA_RANGE_NAME:-google-managed-services-${VPC_NETWORK}}"
|
||||
PSA_PREFIX_LENGTH="${PSA_PREFIX_LENGTH:-16}" # /16 is GCP's recommendation; reserved, not consumed
|
||||
|
||||
DB_INSTANCE="${DB_INSTANCE:-claude-gateway-db}"
|
||||
DB_VERSION="${DB_VERSION:-POSTGRES_16}" # PG14+ supported; 16 is the recommended default (§4)
|
||||
DB_TIER="${DB_TIER:-db-g1-small}"
|
||||
DB_NAME="${DB_NAME:-claude_gateway}"
|
||||
DB_USER="${DB_USER:-gateway}"
|
||||
|
||||
SECRET_NAME="${SECRET_NAME:-gateway-postgres-url}" # §5 store.postgres_url
|
||||
JWT_SECRET_NAME="${JWT_SECRET_NAME:-gateway-jwt-secret}" # §5 session.jwt_secret
|
||||
|
||||
GATEWAY_YAML="${GATEWAY_YAML:-./gateway.yaml}" # §6 config file
|
||||
CONFIG_SECRET="${CONFIG_SECRET:-gateway-config}" # §6 mounted at /etc/claude/gateway.yaml
|
||||
|
||||
# §7 Cloud Run deploy
|
||||
SERVICE_NAME="${SERVICE_NAME:-claude-gateway}"
|
||||
OIDC_SECRET_NAME="${OIDC_SECRET_NAME:-gateway-oidc-client-secret}" # operator-created (Google OAuth client)
|
||||
DEPLOY="${DEPLOY:-1}" # set DEPLOY=0 to provision only, no Cloud Run deploy
|
||||
INGRESS="${INGRESS:-internal}" # internal (default; no public URL) | internal-and-cloud-load-balancing (only if you front it with your own internal ALB)
|
||||
MAX_INSTANCES="${MAX_INSTANCES:-8}" # keep MAX_INSTANCES × store.max_connections (default 5) below the DB tier's max_connections (~50 on db-g1-small); raise the tier before raising this
|
||||
|
||||
# ---- helpers ----------------------------------------------------------------
|
||||
log() { printf '\n==> %s\n' "$*"; }
|
||||
skip() { printf ' (exists) %s\n' "$*"; }
|
||||
curl_https() { curl --proto '=https' --proto-redir '=https' --tlsv1.2 "$@"; } # refuse plaintext/protocol-downgrade
|
||||
sha_of() { openssl dgst -sha256 "$1" | awk '{print $NF}'; } # openssl avoids shasum/sha256sum portability gaps
|
||||
|
||||
if [[ -z "${PROJECT_ID}" ]]; then
|
||||
echo "ERROR: PROJECT_ID is not set and no gcloud default project is configured." >&2
|
||||
echo " Set it with: export PROJECT_ID=<your-project> (or 'gcloud config set project ...')" >&2
|
||||
exit 1
|
||||
fi
|
||||
# VERSION tags the image and selects the public Claude Code release to download.
|
||||
# The first resolved value is pinned to ${VERSION_FILE} so the documented
|
||||
# re-runs (fill gateway.yaml -> re-run; set public_url -> re-run) don't silently
|
||||
# build and deploy a newer release mid-bootstrap.
|
||||
if [[ -z "${VERSION}" && -f "${VERSION_FILE}" ]]; then
|
||||
VERSION="$(< "${VERSION_FILE}")"
|
||||
log "Using release pinned in ${VERSION_FILE}: ${VERSION} (delete the file or set VERSION to change it)"
|
||||
elif [[ -z "${VERSION}" ]]; then
|
||||
# /latest is the channel the official installer (claude.ai/install.sh) uses.
|
||||
VERSION="$(curl_https -fsSL "${RELEASES_URL}/latest" | tr -d '[:space:]' || true)"
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
echo "ERROR: could not resolve the latest release from ${RELEASES_URL}/latest." >&2
|
||||
echo " Set VERSION to a Claude Code release version, e.g. export VERSION=2.1.195" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "VERSION not set — using latest Claude Code release: ${VERSION}"
|
||||
fi
|
||||
# Reject non-version content (e.g. an HTML error page served with HTTP 200)
|
||||
# before it reaches the image tag and download URLs.
|
||||
if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
echo "ERROR: '${VERSION}' is not a release version (from VERSION, ${VERSION_FILE}, or ${RELEASES_URL}/latest)." >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "${VERSION}" > "${VERSION_FILE}"
|
||||
IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO}/${IMAGE_NAME}:${VERSION}"
|
||||
# Claude Code only connects to a gateway whose hostname resolves to private
|
||||
# addresses (a client-side /login check), so public ingress can never serve
|
||||
# clients — mirror the terraform module's validation and refuse it up front.
|
||||
if [[ "${INGRESS}" != "internal" && "${INGRESS}" != "internal-and-cloud-load-balancing" ]]; then
|
||||
echo "ERROR: INGRESS must be 'internal' or 'internal-and-cloud-load-balancing' — Claude Code's" >&2
|
||||
echo " /login only accepts gateway hosts on private addresses, so public ingress cannot serve clients." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Project: ${PROJECT_ID} Region: ${REGION} VPC: ${VPC_NETWORK}"
|
||||
|
||||
# ---- 1 Project & API setup ------------------------------------------------
|
||||
# walkthrough §1 list (aiplatform, artifactregistry, sqladmin, secretmanager, iamcredentials)
|
||||
# plus iam/compute/servicenetworking required for the SA + private-IP networking below.
|
||||
# container.googleapis.com is for the GKE track (§7a) — harmless if you stay on Cloud Run.
|
||||
# We pass --project on every call rather than mutating your gcloud config.
|
||||
log "Enabling required APIs (§1)"
|
||||
gcloud services enable \
|
||||
aiplatform.googleapis.com \
|
||||
artifactregistry.googleapis.com \
|
||||
sqladmin.googleapis.com \
|
||||
secretmanager.googleapis.com \
|
||||
iamcredentials.googleapis.com \
|
||||
iam.googleapis.com \
|
||||
compute.googleapis.com \
|
||||
container.googleapis.com \
|
||||
servicenetworking.googleapis.com \
|
||||
run.googleapis.com \
|
||||
--project="${PROJECT_ID}"
|
||||
|
||||
# ---- 2 Service account & IAM ----------------------------------------------
|
||||
log "Creating service account ${SA_EMAIL} and granting project roles (§2)"
|
||||
if gcloud iam service-accounts describe "${SA_EMAIL}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "service account ${SA_EMAIL}"
|
||||
else
|
||||
gcloud iam service-accounts create "${SA_NAME}" \
|
||||
--display-name="Claude Gateway" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# add-iam-policy-binding is idempotent (re-adding an existing binding is a no-op).
|
||||
# --condition=None avoids the interactive condition prompt in non-interactive runs.
|
||||
#
|
||||
# Only aiplatform.user is granted: the gateway reaches Cloud SQL over the VPC at
|
||||
# its PRIVATE IP with a password user (§4/§7b — direct TCP, not the Cloud SQL
|
||||
# Auth Proxy / connector), so it never calls cloudsql.instances.connect and no
|
||||
# roles/cloudsql.client grant is needed. Direct private-IP is used because the
|
||||
# gateway's store is a plain postgres_url — no proxy sidecar/socket plumbing,
|
||||
# one less moving part, and the connection string is portable across Cloud Run
|
||||
# and GKE.
|
||||
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/aiplatform.user" --condition=None >/dev/null # Agent Platform inference (§2)
|
||||
|
||||
# ---- 3 Build & push image to Artifact Registry ----------------------------
|
||||
log "Ensuring Artifact Registry repo and image (§3)"
|
||||
if gcloud artifacts repositories describe "${AR_REPO}" \
|
||||
--location="${REGION}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "Artifact Registry repo ${AR_REPO}"
|
||||
else
|
||||
gcloud artifacts repositories create "${AR_REPO}" \
|
||||
--repository-format=docker --location="${REGION}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# Image is the expensive, already-done step: skip the build+push entirely if the
|
||||
# tag already exists in the registry.
|
||||
if gcloud artifacts docker images describe "${IMAGE}" >/dev/null 2>&1; then
|
||||
skip "image ${IMAGE}"
|
||||
else
|
||||
# The public Claude Code release includes the gateway subcommand, so the
|
||||
# binary comes straight from the release endpoint, verified against the
|
||||
# release manifest's sha256. A pre-existing ${CLAUDE_BINARY} (stale version,
|
||||
# interrupted download, hand-placed file) is verified the same way and
|
||||
# re-downloaded on mismatch, so an unverified binary can never reach the image.
|
||||
manifest="$(curl_https -fsSL "${RELEASES_URL}/${VERSION}/manifest.json" | tr -d '[:space:]' || true)"
|
||||
sha_re='"linux-x64"[^}]*"checksum":"([a-f0-9]{64})"' # structure-based: survives pretty-printed, minified, and one-line-per-platform manifests
|
||||
if [[ ! "${manifest}" =~ ${sha_re} ]]; then
|
||||
echo "ERROR: could not read the linux-x64 sha256 from ${RELEASES_URL}/${VERSION}/manifest.json — refusing to build." >&2
|
||||
exit 1
|
||||
fi
|
||||
expected_sha="${BASH_REMATCH[1]}"
|
||||
if [[ -f "${CLAUDE_BINARY}" && "$(sha_of "${CLAUDE_BINARY}")" == "${expected_sha}" ]]; then
|
||||
skip "binary ${CLAUDE_BINARY} (sha256 matches release ${VERSION})"
|
||||
else
|
||||
if [[ -f "${CLAUDE_BINARY}" ]]; then
|
||||
log "Existing ${CLAUDE_BINARY} does not match release ${VERSION} — re-downloading"
|
||||
else
|
||||
log "Downloading Claude Code ${VERSION} (linux-x64) from ${RELEASES_URL}"
|
||||
fi
|
||||
# Until verification passes, ANY exit (curl failure, set -e, signal, the
|
||||
# error exit below) removes the file, so a partial download can't be
|
||||
# silently picked up by a later run.
|
||||
trap 'rm -f "${CLAUDE_BINARY}"' EXIT INT TERM
|
||||
curl_https -fL -o "${CLAUDE_BINARY}" "${RELEASES_URL}/${VERSION}/linux-x64/claude"
|
||||
actual_sha="$(sha_of "${CLAUDE_BINARY}")"
|
||||
if [[ "${actual_sha}" != "${expected_sha}" ]]; then
|
||||
echo "ERROR: sha256 of ${CLAUDE_BINARY} is ${actual_sha} but the release manifest says ${expected_sha} — refusing to build." >&2
|
||||
exit 1
|
||||
fi
|
||||
trap - EXIT INT TERM
|
||||
log "Verified binary sha256 ${actual_sha}"
|
||||
fi
|
||||
# Optional out-of-band pin, checked even for a pre-existing binary: the
|
||||
# manifest shares an origin with the binary, so it can't defend against a
|
||||
# compromised endpoint — CLAUDE_SHA256 can.
|
||||
if [[ -n "${CLAUDE_SHA256}" && "$(sha_of "${CLAUDE_BINARY}")" != "${CLAUDE_SHA256}" ]]; then
|
||||
echo "ERROR: sha256 of ${CLAUDE_BINARY} does not match CLAUDE_SHA256 (${CLAUDE_SHA256}) — refusing to build." >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "${CLAUDE_BINARY}"
|
||||
log "Building and pushing ${IMAGE}"
|
||||
gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
|
||||
# Cloud Run requires linux/amd64. --platform forces it (e.g. when building on an
|
||||
# Apple Silicon Mac), and --provenance=false keeps buildx from wrapping the result
|
||||
# in an OCI image index that Cloud Run rejects ("manifest ... must support amd64/linux").
|
||||
docker build --platform=linux/amd64 --provenance=false \
|
||||
-f "${DOCKERFILE}" --build-arg CLAUDE_BINARY="${CLAUDE_BINARY}" -t "${IMAGE}" .
|
||||
docker push "${IMAGE}"
|
||||
fi
|
||||
|
||||
# ---- 4 VPC + Private Services Access (private-IP prerequisite) -------------
|
||||
log "Creating VPC network and subnet"
|
||||
if gcloud compute networks describe "${VPC_NETWORK}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "network ${VPC_NETWORK}"
|
||||
else
|
||||
gcloud compute networks create "${VPC_NETWORK}" \
|
||||
--subnet-mode=custom --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
if gcloud compute networks subnets describe "${SUBNET}" \
|
||||
--region="${REGION}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "subnet ${SUBNET}"
|
||||
else
|
||||
gcloud compute networks subnets create "${SUBNET}" \
|
||||
--network="${VPC_NETWORK}" --region="${REGION}" \
|
||||
--range="${SUBNET_RANGE}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
log "Configuring Private Services Access (allocated range + VPC peering)"
|
||||
if gcloud compute addresses describe "${PSA_RANGE_NAME}" \
|
||||
--global --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "allocated range ${PSA_RANGE_NAME}"
|
||||
else
|
||||
gcloud compute addresses create "${PSA_RANGE_NAME}" \
|
||||
--global --purpose=VPC_PEERING --prefix-length="${PSA_PREFIX_LENGTH}" \
|
||||
--network="${VPC_NETWORK}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
if gcloud services vpc-peerings list --network="${VPC_NETWORK}" --project="${PROJECT_ID}" \
|
||||
--format='value(peering)' 2>/dev/null | grep -q servicenetworking; then
|
||||
skip "servicenetworking VPC peering"
|
||||
else
|
||||
gcloud services vpc-peerings connect \
|
||||
--service=servicenetworking.googleapis.com \
|
||||
--ranges="${PSA_RANGE_NAME}" \
|
||||
--network="${VPC_NETWORK}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# ---- 4 Cloud SQL instance (private IP only) -------------------------------
|
||||
log "Creating Cloud SQL instance ${DB_INSTANCE} (private IP only)"
|
||||
if gcloud sql instances describe "${DB_INSTANCE}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "instance ${DB_INSTANCE}"
|
||||
else
|
||||
gcloud sql instances create "${DB_INSTANCE}" \
|
||||
--database-version="${DB_VERSION}" \
|
||||
--tier="${DB_TIER}" \
|
||||
--region="${REGION}" \
|
||||
--network="projects/${PROJECT_ID}/global/networks/${VPC_NETWORK}" \
|
||||
--no-assign-ip \
|
||||
--project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
log "Creating database ${DB_NAME}"
|
||||
if gcloud sql databases describe "${DB_NAME}" \
|
||||
--instance="${DB_INSTANCE}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "database ${DB_NAME}"
|
||||
else
|
||||
gcloud sql databases create "${DB_NAME}" \
|
||||
--instance="${DB_INSTANCE}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# hex (not base64) keeps the password URL-safe for the connection string below.
|
||||
log "Creating database user ${DB_USER}"
|
||||
DB_PASSWORD=""
|
||||
if gcloud sql users list --instance="${DB_INSTANCE}" --project="${PROJECT_ID}" \
|
||||
--format='value(name)' 2>/dev/null | grep -qx "${DB_USER}"; then
|
||||
if gcloud secrets describe "${SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "user ${DB_USER} (password unchanged; secret not rewritten)"
|
||||
else
|
||||
# Self-heal: a previous run died after creating the user but before writing
|
||||
# the connection-string secret, losing the only copy of the password. The
|
||||
# secret is the password's only consumer, so resetting it is safe and keeps
|
||||
# re-runs able to recover from any partial state.
|
||||
log "User ${DB_USER} exists but secret ${SECRET_NAME} is missing — resetting password"
|
||||
DB_PASSWORD="$(openssl rand -hex 24)"
|
||||
gcloud sql users set-password "${DB_USER}" \
|
||||
--instance="${DB_INSTANCE}" --password="${DB_PASSWORD}" \
|
||||
--project="${PROJECT_ID}"
|
||||
fi
|
||||
else
|
||||
DB_PASSWORD="$(openssl rand -hex 24)"
|
||||
gcloud sql users create "${DB_USER}" \
|
||||
--instance="${DB_INSTANCE}" --password="${DB_PASSWORD}" \
|
||||
--project="${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# ---- 5 Connection string -> Secret Manager + secretAccessor ---------------
|
||||
PRIVATE_IP="$(gcloud sql instances describe "${DB_INSTANCE}" --project="${PROJECT_ID}" \
|
||||
--format='value(ipAddresses[0].ipAddress)')"
|
||||
|
||||
if [[ -n "${DB_PASSWORD}" ]]; then
|
||||
# direct private-IP form, ?sslmode=require (guide §4)
|
||||
CONN="postgres://${DB_USER}:${DB_PASSWORD}@${PRIVATE_IP}:5432/${DB_NAME}?sslmode=require"
|
||||
log "Storing connection string in Secret Manager secret ${SECRET_NAME}"
|
||||
if gcloud secrets describe "${SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
printf '%s' "${CONN}" | gcloud secrets versions add "${SECRET_NAME}" \
|
||||
--data-file=- --project="${PROJECT_ID}"
|
||||
else
|
||||
printf '%s' "${CONN}" | gcloud secrets create "${SECRET_NAME}" \
|
||||
--replication-policy=automatic --data-file=- --project="${PROJECT_ID}"
|
||||
fi
|
||||
else
|
||||
log "Skipping secret write (user already existed, password not available this run)"
|
||||
fi
|
||||
|
||||
if gcloud secrets describe "${SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
log "Granting ${SA_EMAIL} secretAccessor on ${SECRET_NAME}"
|
||||
gcloud secrets add-iam-policy-binding "${SECRET_NAME}" \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--condition=None --project="${PROJECT_ID}" >/dev/null
|
||||
fi
|
||||
|
||||
# JWT signing secret — generated once (re-runs do NOT rotate it).
|
||||
log "Ensuring JWT signing secret ${JWT_SECRET_NAME} (§5)"
|
||||
if gcloud secrets describe "${JWT_SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
skip "secret ${JWT_SECRET_NAME}"
|
||||
else
|
||||
openssl rand -base64 32 | tr -d '\n' | gcloud secrets create "${JWT_SECRET_NAME}" \
|
||||
--replication-policy=automatic --data-file=- --project="${PROJECT_ID}"
|
||||
fi
|
||||
gcloud secrets add-iam-policy-binding "${JWT_SECRET_NAME}" \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--condition=None --project="${PROJECT_ID}" >/dev/null
|
||||
|
||||
# OIDC client secret — operator-created (the script can't generate it). Grant
|
||||
# accessor here once it exists so the deploy step doesn't fail on permission.
|
||||
if gcloud secrets describe "${OIDC_SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
log "Granting ${SA_EMAIL} secretAccessor on ${OIDC_SECRET_NAME}"
|
||||
gcloud secrets add-iam-policy-binding "${OIDC_SECRET_NAME}" \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--condition=None --project="${PROJECT_ID}" >/dev/null
|
||||
fi
|
||||
|
||||
# ---- 6 gateway.yaml -> Secret Manager (gateway-config) --------------------
|
||||
# Published only when fully filled in: refuse to push a config that still has
|
||||
# REPLACE_ME placeholders (checked on non-comment lines so commented examples
|
||||
# and this file's header don't trip the guard).
|
||||
log "Publishing ${GATEWAY_YAML} as Secret Manager secret ${CONFIG_SECRET} (§6)"
|
||||
if [[ ! -f "${GATEWAY_YAML}" ]]; then
|
||||
echo " (skip) ${GATEWAY_YAML} not found — run 'cp gateway.yaml.example gateway.yaml', fill it in, then re-run (§6)."
|
||||
elif grep -vE '^[[:space:]]*#' "${GATEWAY_YAML}" | grep -q 'REPLACE_ME'; then
|
||||
echo " (skip) ${GATEWAY_YAML} still has REPLACE_ME placeholders to fill:"
|
||||
grep -nE 'REPLACE_ME' "${GATEWAY_YAML}" | grep -vE '^[0-9]+:[[:space:]]*#' | sed 's/^/ /'
|
||||
echo " Fill them in, then re-run to publish ${CONFIG_SECRET}."
|
||||
else
|
||||
if gcloud secrets describe "${CONFIG_SECRET}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
gcloud secrets versions add "${CONFIG_SECRET}" \
|
||||
--data-file="${GATEWAY_YAML}" --project="${PROJECT_ID}"
|
||||
else
|
||||
gcloud secrets create "${CONFIG_SECRET}" --replication-policy=automatic \
|
||||
--data-file="${GATEWAY_YAML}" --project="${PROJECT_ID}"
|
||||
fi
|
||||
gcloud secrets add-iam-policy-binding "${CONFIG_SECRET}" \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--condition=None --project="${PROJECT_ID}" >/dev/null
|
||||
fi
|
||||
|
||||
# ---- 7 Cloud Run deploy (Direct VPC egress) -------------------------------
|
||||
# Direct VPC egress (--network/--subnet/--vpc-egress) puts the service on the
|
||||
# VPC so it reaches the Cloud SQL PRIVATE IP directly — matching the private-IP
|
||||
# connection string in the postgres-url secret. private-ranges-only keeps public
|
||||
# egress (Agent Platform, accounts.google.com) off the VPC, so no Cloud NAT is needed.
|
||||
# We deliberately do NOT use --add-cloudsql-instances (that's the Auth Proxy /
|
||||
# socket path, which would need a different connection string).
|
||||
#
|
||||
# Secrets: gateway.yaml is mounted as a FILE at /etc/claude (alone in its dir).
|
||||
# The JWT / OIDC / Postgres secrets are injected as ENV VARS — Cloud Run cannot
|
||||
# mount multiple secrets into one directory, and gateway.yaml references them via
|
||||
# ${ENV_VAR}. (See the env-var names in gateway.yaml: GATEWAY_JWT_SECRET etc.)
|
||||
#
|
||||
# Self-gating: deploy only once its inputs exist (config secret published + the
|
||||
# operator-provided OIDC client secret). On a first run these are missing and it
|
||||
# cleanly skips.
|
||||
RUN_URL=""
|
||||
missing=""
|
||||
gcloud secrets describe "${CONFIG_SECRET}" --project="${PROJECT_ID}" >/dev/null 2>&1 || missing="${missing} ${CONFIG_SECRET}"
|
||||
gcloud secrets describe "${OIDC_SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1 || missing="${missing} ${OIDC_SECRET_NAME}"
|
||||
# Also gate on the postgres-url secret (referenced by --set-secrets below): if it
|
||||
# is somehow absent, skip with a clear message rather than failing the deploy with
|
||||
# a raw Cloud Run missing-secret error.
|
||||
gcloud secrets describe "${SECRET_NAME}" --project="${PROJECT_ID}" >/dev/null 2>&1 || missing="${missing} ${SECRET_NAME}"
|
||||
|
||||
if [[ "${DEPLOY}" != "1" ]]; then
|
||||
log "Skipping Cloud Run deploy (DEPLOY=${DEPLOY}) (§7)"
|
||||
elif [[ -n "${missing// }" ]]; then
|
||||
log "Skipping Cloud Run deploy — missing secret(s):${missing} (§7)"
|
||||
echo " Fill ${GATEWAY_YAML} and re-run to publish ${CONFIG_SECRET}; create ${OIDC_SECRET_NAME}"
|
||||
echo " from the Google OAuth client. Then re-run to deploy."
|
||||
else
|
||||
SECRET_MOUNTS="/etc/claude/gateway.yaml=${CONFIG_SECRET}:latest" # file mount (alone in /etc/claude)
|
||||
SECRET_MOUNTS="${SECRET_MOUNTS},GATEWAY_JWT_SECRET=${JWT_SECRET_NAME}:latest" # env var
|
||||
SECRET_MOUNTS="${SECRET_MOUNTS},OIDC_CLIENT_SECRET=${OIDC_SECRET_NAME}:latest" # env var
|
||||
SECRET_MOUNTS="${SECRET_MOUNTS},GATEWAY_POSTGRES_URL=${SECRET_NAME}:latest" # env var
|
||||
|
||||
log "Deploying Cloud Run service ${SERVICE_NAME} (§7b, Direct VPC egress)"
|
||||
# Deploy private (--no-allow-unauthenticated avoids the interactive prompt and
|
||||
# keeps allUsers OUT of the deploy, so a Domain-Restricted-Sharing org doesn't
|
||||
# fail the deploy on the IAM step). Public access is attempted separately below.
|
||||
#
|
||||
# --ingress is passed EXPLICITLY because it is sticky across redeploys (omitting
|
||||
# it keeps the previous value). The default, internal, keeps the *.run.app URL
|
||||
# off the public internet — reachable only from this VPC, or from corp networks
|
||||
# with the PSC endpoint + private run.app DNS plumbing (see terraform/README.md
|
||||
# "Private access"). Public ingress cannot serve clients (see the INGRESS
|
||||
# guard at the top of this script), so the two-pass OAuth bootstrap has to be
|
||||
# completed from inside the VPC (or a PSC-connected corp network). Use
|
||||
# internal-and-cloud-load-balancing instead if you front the service with
|
||||
# your own internal ALB.
|
||||
#
|
||||
# --timeout=3600 raises Cloud Run's default 300s request timeout, which would
|
||||
# otherwise cut off long streaming /v1/messages responses mid-stream.
|
||||
#
|
||||
# --max-instances bounds the Postgres connection footprint: each instance
|
||||
# opens a pool of up to 5 connections (store.max_connections default) and
|
||||
# db-g1-small caps at ~50 max_connections, so the default ceiling of 100
|
||||
# instances would crash-loop new instances under load. Keep
|
||||
# max-instances × 5 below the DB tier's max_connections; raise the DB tier
|
||||
# (or set store.max_connections lower) before raising this.
|
||||
gcloud run deploy "${SERVICE_NAME}" \
|
||||
--image="${IMAGE}" \
|
||||
--region="${REGION}" \
|
||||
--service-account="${SA_EMAIL}" \
|
||||
--min-instances=1 \
|
||||
--max-instances="${MAX_INSTANCES}" \
|
||||
--port=8080 \
|
||||
--timeout=3600 \
|
||||
--ingress="${INGRESS}" \
|
||||
--network="${VPC_NETWORK}" \
|
||||
--subnet="${SUBNET}" \
|
||||
--vpc-egress=private-ranges-only \
|
||||
--set-secrets="${SECRET_MOUNTS}" \
|
||||
--no-allow-unauthenticated \
|
||||
--project="${PROJECT_ID}"
|
||||
|
||||
# The gateway runs its OWN OIDC, so the Cloud Run IAM layer must allow
|
||||
# unauthenticated. Attempt it separately and tolerate failure: Domain Restricted
|
||||
# Sharing (iam.allowedPolicyMemberDomains) blocks allUsers in hardened orgs.
|
||||
log "Granting public invoker (allUsers) — required for the gateway's OIDC login"
|
||||
if gcloud run services add-iam-policy-binding "${SERVICE_NAME}" \
|
||||
--region="${REGION}" --member=allUsers --role=roles/run.invoker \
|
||||
--project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||
echo " public invoker granted."
|
||||
else
|
||||
echo " WARN: allUsers rejected (likely Domain Restricted Sharing). The service is"
|
||||
echo " deployed but the invoker IAM check is still enabled, so requests 403"
|
||||
echo " before reaching the container. Preferred fix (where available):"
|
||||
echo " gcloud run services update ${SERVICE_NAME} --no-invoker-iam-check \\"
|
||||
echo " --region=${REGION} --project=${PROJECT_ID}"
|
||||
echo " Alternatively: request a DRS exception for ${SERVICE_NAME}, or use the GKE"
|
||||
echo " track, which exposes the gateway at the network layer with no allUsers"
|
||||
echo " binding. An LB is NOT a fix — it does not bypass the invoker IAM check."
|
||||
fi
|
||||
|
||||
RUN_URL="$(gcloud run services describe "${SERVICE_NAME}" --region="${REGION}" \
|
||||
--project="${PROJECT_ID}" --format='value(status.url)')"
|
||||
log "Cloud Run URL: ${RUN_URL}"
|
||||
|
||||
# public_url is now required (config validation refuses a non-loopback bind
|
||||
# without it), so the template ships a placeholder for the first pass. Once we
|
||||
# know the real URL, warn on any mismatch so the operator doesn't leave the
|
||||
# placeholder — or a stale hostname — in place. Normalize quotes / inline
|
||||
# comments / a trailing slash so schema-equivalent spellings compare equal.
|
||||
# Only checked with internal ingress, where public_url should be the run.app
|
||||
# URL; behind an internal ALB it is the ALB hostname, which this script
|
||||
# cannot know.
|
||||
CFG_PUBLIC_URL="$(grep -E '^[[:space:]]*public_url:' "${GATEWAY_YAML}" 2>/dev/null \
|
||||
| head -1 \
|
||||
| sed -E 's/^[[:space:]]*public_url:[[:space:]]*//; s/[[:space:]]+#.*$//; s/[[:space:]]*$//' \
|
||||
|| true)"
|
||||
CFG_PUBLIC_URL="${CFG_PUBLIC_URL#[\'\"]}"; CFG_PUBLIC_URL="${CFG_PUBLIC_URL%[\'\"]}"
|
||||
CFG_PUBLIC_URL="${CFG_PUBLIC_URL%/}"
|
||||
if [[ "${INGRESS}" == "internal" && -n "${RUN_URL}" && "${CFG_PUBLIC_URL}" != "${RUN_URL%/}" ]]; then
|
||||
echo " NOTE — ${GATEWAY_YAML} has public_url: ${CFG_PUBLIC_URL:-<unset>}"
|
||||
echo " but this service's URL is ${RUN_URL}."
|
||||
echo " Set listen.public_url to ${RUN_URL} (or your LB hostname) and re-run."
|
||||
fi
|
||||
|
||||
if [[ -n "${RUN_URL}" ]]; then
|
||||
# gcloud run deploy already fails the script if the revision can't boot (it
|
||||
# waits for the Ready condition), so what's left to verify is that the
|
||||
# gateway is serving. The OAuth discovery document below returns 200 only
|
||||
# after config load, OIDC discovery, upstream construction, and Postgres
|
||||
# migration all succeed, so it doubles as an end-to-end boot check (the
|
||||
# readiness probe proper is GET /readyz). With internal ingress the URL is
|
||||
# reachable only from inside the VPC (or a PSC-connected corp network), so
|
||||
# verification is left to the operator rather than attempted from here.
|
||||
log "Verify the gateway is serving (from inside the VPC, or a PSC-connected corp network):"
|
||||
echo " curl -s ${RUN_URL}/.well-known/oauth-authorization-server"
|
||||
echo " If it isn't responding yet, check logs:"
|
||||
echo " gcloud run services logs read ${SERVICE_NAME} --region=${REGION} --project=${PROJECT_ID}"
|
||||
|
||||
log "Finish the OAuth bootstrap:"
|
||||
echo " 1. Register this redirect URI on the Google OAuth client: ${RUN_URL}/oauth/callback"
|
||||
echo " 2. Set listen.public_url in ${GATEWAY_YAML} to ${RUN_URL}, then re-run: INGRESS=${INGRESS} ./setup.sh"
|
||||
echo " (republishes ${CONFIG_SECRET} and redeploys so the IdP redirect_uri matches)."
|
||||
echo " With INGRESS=internal-and-cloud-load-balancing, use your internal ALB hostname"
|
||||
echo " instead of the run.app URL in both steps."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- summary ----------------------------------------------------------------
|
||||
cat <<EOF
|
||||
|
||||
==> Done.
|
||||
|
||||
Service account ${SA_EMAIL}
|
||||
roles: aiplatform.user, secretmanager.secretAccessor
|
||||
Image ${IMAGE}
|
||||
Instance ${DB_INSTANCE}
|
||||
Connection name ${PROJECT_ID}:${REGION}:${DB_INSTANCE}
|
||||
Private IP ${PRIVATE_IP}
|
||||
Database / user ${DB_NAME} / ${DB_USER}
|
||||
Secrets ${SECRET_NAME}, ${JWT_SECRET_NAME}, ${CONFIG_SECRET}
|
||||
Cloud Run service ${SERVICE_NAME} -> ${RUN_URL:-(not deployed yet)} (ingress: ${INGRESS})
|
||||
|
||||
Next steps (see https://code.claude.com/docs/en/claude-apps-gateway-on-gcp):
|
||||
- Create the one operator-provided secret (from the Google Cloud Console OAuth client):
|
||||
printf '%s' "<client-secret>" | gcloud secrets create ${OIDC_SECRET_NAME} \\
|
||||
--data-file=- --project="${PROJECT_ID}"
|
||||
setup.sh grants ${SA_EMAIL} secretAccessor on it on the next re-run.
|
||||
- Fill in the REPLACE_ME values in ${GATEWAY_YAML}, then re-run: setup.sh publishes
|
||||
${CONFIG_SECRET} and deploys ${SERVICE_NAME} once both secrets exist.
|
||||
- After the first deploy: set listen.public_url to the Cloud Run URL above (or your
|
||||
internal ALB hostname) and register <url>/oauth/callback on the Google OAuth client,
|
||||
then re-run to redeploy.
|
||||
- The gateway runs its own schema migrations at boot, so ${DB_USER} needs CREATE TABLE.
|
||||
EOF
|
||||
13
examples/gateway/gcp/terraform/.gitignore
vendored
Normal file
13
examples/gateway/gcp/terraform/.gitignore
vendored
Normal 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
|
||||
160
examples/gateway/gcp/terraform/README.md
Normal file
160
examples/gateway/gcp/terraform/README.md
Normal 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) §1–7): APIs →
|
||||
service account + IAM → Artifact Registry repo → VPC + Private Services Access →
|
||||
private-IP Cloud SQL (PG16) → secrets → Cloud Run with Direct VPC egress.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `versions.tf` | Provider pins (google, random) |
|
||||
| `variables.tf` | All inputs (defaults match `setup.sh`'s) |
|
||||
| `main.tf` | Resources |
|
||||
| `outputs.tf` | Service URL, OAuth redirect URI, SA, DB info |
|
||||
| `terraform.tfvars.example` | Copy to `terraform.tfvars` and edit |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **`../gateway.yaml` created and filled in** — copy the template first:
|
||||
`cp ../gateway.yaml.example ../gateway.yaml`, then replace every `REPLACE_ME`
|
||||
(Terraform reads this file and enforces no `REPLACE_ME` via a precondition).
|
||||
Leave `public_url` at its placeholder for the first apply; set it to the
|
||||
`run.app` URL (the `service_url` output) or your LB hostname and re-apply.
|
||||
`gateway.yaml` is gitignored; the committed template is `gateway.yaml.example`.
|
||||
2. A **remote backend** for shared use (see below). State holds secrets — never commit it.
|
||||
|
||||
## Deploy
|
||||
|
||||
Terraform creates the Artifact Registry repo but does **not** build/push the
|
||||
image, so the apply is two passes: a targeted apply to create the repo, then
|
||||
build/push, then the full apply.
|
||||
|
||||
```bash
|
||||
cp terraform.tfvars.example terraform.tfvars # edit it
|
||||
terraform init
|
||||
|
||||
# 1. Create just the Artifact Registry repo (the -target warning is expected):
|
||||
terraform apply -target=google_artifact_registry_repository.repo
|
||||
|
||||
# 2. Download the public Claude Code linux-x64 release binary (it includes the
|
||||
# `gateway` subcommand; the Dockerfile picks it up at gcp/claude), verify its
|
||||
# sha256 against the release manifest, then build and push the image:
|
||||
BASE="https://downloads.claude.ai/claude-code-releases"
|
||||
VERSION="$(curl -fsSL --proto '=https' "${BASE}/latest")"
|
||||
curl -fL --proto '=https' --proto-redir '=https' -o ../claude \
|
||||
"${BASE}/${VERSION}/linux-x64/claude"
|
||||
WANT="$(curl -fsSL --proto '=https' "${BASE}/${VERSION}/manifest.json" \
|
||||
| tr -d '[:space:]' | grep -oE '"linux-x64"[^}]*' | grep -oE '[a-f0-9]{64}' | head -1)"
|
||||
[ "$(openssl dgst -sha256 ../claude | awk '{print $NF}')" = "${WANT}" ] \
|
||||
&& echo "sha256 OK" || { echo "checksum mismatch" >&2; rm -f ../claude; }
|
||||
gcloud auth configure-docker us-east5-docker.pkg.dev --quiet
|
||||
docker build --platform=linux/amd64 --provenance=false \
|
||||
-f ../Dockerfile -t "us-east5-docker.pkg.dev/<project>/claude-gateway/gateway:${VERSION}" ..
|
||||
docker push "us-east5-docker.pkg.dev/<project>/claude-gateway/gateway:${VERSION}"
|
||||
|
||||
# 3. Full apply:
|
||||
terraform apply
|
||||
```
|
||||
|
||||
(`../setup.sh` §3 automates the same download-and-verify.)
|
||||
|
||||
Set in `terraform.tfvars`:
|
||||
|
||||
- `project_id`, `region`
|
||||
- `image_tag` (after building/pushing — step 2 above)
|
||||
- **`oidc_client_secret`** — required (the Cloud Run service mounts `latest` of
|
||||
this secret; with no version the deploy fails). Terraform creates the
|
||||
secret + version from it.
|
||||
- `invoker_iam_disabled` / `allow_unauthenticated` — the gateway runs its own
|
||||
OIDC, so the Cloud Run invoker IAM check must be opened or disabled.
|
||||
**Preferred:** `invoker_iam_disabled = true` (no `allUsers` binding; works
|
||||
under Domain Restricted Sharing). **Fallback:** `allow_unauthenticated = true`
|
||||
grants `allUsers` `run.invoker` — fine on a normal org, but DRS orgs reject
|
||||
`allUsers` (set it `false` there, since an LB does **not** bypass the IAM
|
||||
check). If both paths are blocked by org policy, use the GKE track.
|
||||
- `ingress` — defaults to **internal-only** (no public URL). Claude Code's `/login`
|
||||
only accepts gateway hosts on private addresses, so public ingress cannot serve
|
||||
clients; the two-pass OAuth bootstrap must be completed from inside the VPC (or a
|
||||
PSC-connected corp network). See "Private access" below.
|
||||
|
||||
Tear down a trial with `terraform destroy`: set `deletion_protection = false`,
|
||||
run `terraform apply` to record that in state (the provider checks the value in
|
||||
**state**, not config, so destroy would still refuse otherwise), then `terraform
|
||||
destroy`. The destroy will stop at the VPC network
|
||||
because the Private Services Access peering is intentionally left in place
|
||||
(`deletion_policy = ABANDON` — see Guard rails below); finish by deleting the
|
||||
peering manually once the Cloud SQL instance is gone, then re-run destroy:
|
||||
|
||||
```bash
|
||||
gcloud services vpc-peerings delete --service=servicenetworking.googleapis.com \
|
||||
--network=cc-gateway-vpc --project=<project>
|
||||
terraform destroy
|
||||
```
|
||||
|
||||
## Guard rails
|
||||
|
||||
Tuned so accidental deletion is hard but greenfield teardown stays easy:
|
||||
|
||||
- `deletion_protection = true` (variable, default true) on Cloud SQL and Cloud Run —
|
||||
blocks accidental deletion; set `false` when you intend to `terraform destroy`.
|
||||
- `disable_on_destroy = false` on APIs — tearing down config never disables APIs.
|
||||
- `deletion_policy = ABANDON` on the PSA peering — never tears down the
|
||||
service-networking peering automatically (it's shared by every private-IP
|
||||
service on the VPC). On the dedicated VPC this module creates, that means
|
||||
`terraform destroy` stops at the network step; delete the peering manually
|
||||
per the teardown note above.
|
||||
- IAM uses non-authoritative `_member` resources, so other project/secret bindings
|
||||
are never clobbered.
|
||||
|
||||
## Private access (internal ingress) — the default
|
||||
|
||||
By default the service has **no public URL** (`ingress = "INGRESS_TRAFFIC_INTERNAL_ONLY"`),
|
||||
and there is no public-ingress option: Claude Code's `/login` rejects gateway hosts that
|
||||
resolve to public addresses, so public exposure cannot serve clients. Reach the service
|
||||
from inside the VPC, or via the private-access plumbing below.
|
||||
|
||||
With internal-only ingress, `public_url` stays the `run.app` URL (Google-managed cert) —
|
||||
**no load balancer or your own certificate required**. But internal ingress alone does
|
||||
**not** let corporate on-prem clients reach `run.app`; that needs **operator /
|
||||
network-team-owned** plumbing that Cloud Run does **not** create for you (validate it's in
|
||||
place before relying on internal ingress):
|
||||
|
||||
1. A **Private Service Connect endpoint** for Google APIs (an internal VIP in the VPC).
|
||||
2. A **Cloud DNS private zone for `run.app`** resolving `*.run.app` to that endpoint IP.
|
||||
3. **On-prem routing** to the endpoint over Cloud VPN / Interconnect.
|
||||
|
||||
This is normally managed centrally in the network/hub project, so the module does not
|
||||
provision it. See [Private networking and Cloud Run](https://cloud.google.com/run/docs/securing/private-networking).
|
||||
For a greenfield trial without this plumbing, complete the OAuth bootstrap from inside
|
||||
the VPC — e.g. a browser proxied through an in-VPC VM (SSH SOCKS tunnel over IAP).
|
||||
|
||||
For a **custom internal hostname or your own TLS cert**, use
|
||||
`INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER` and front the service with your own internal
|
||||
Application Load Balancer (also not provisioned by this module).
|
||||
|
||||
## Remote state (recommended for teams)
|
||||
|
||||
Add a backend so state is shared and locked (and out of git):
|
||||
|
||||
```hcl
|
||||
# backend.tf
|
||||
terraform {
|
||||
backend "gcs" {
|
||||
bucket = "<your-tf-state-bucket>"
|
||||
prefix = "claude-gateway/cloudrun"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## After deploy
|
||||
|
||||
- `terraform output service_url` / `oauth_redirect_uri`.
|
||||
- Register the redirect URI on the Google OAuth client and make sure
|
||||
`../gateway.yaml` `public_url` matches the host.
|
||||
- Notes: Terraform does not build the image. To ship a new gateway version,
|
||||
rerun the docker build/push under a new tag and bump `image_tag` — a bare
|
||||
re-apply under an unchanged tag does **not** roll a new revision (Cloud Run
|
||||
resolves the tag to a digest only at revision creation, and an unchanged
|
||||
`image` attribute means no new revision).
|
||||
399
examples/gateway/gcp/terraform/main.tf
Normal file
399
examples/gateway/gcp/terraform/main.tf
Normal 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" # Agent Platform inference
|
||||
member = "serviceAccount:${google_service_account.gateway.email}"
|
||||
}
|
||||
|
||||
# ── 3 Artifact Registry repo ────────────────────────────────────────────────
|
||||
# NOTE: image build/push is a separate step (see README) — Terraform only makes the repo.
|
||||
resource "google_artifact_registry_repository" "repo" {
|
||||
project = var.project_id
|
||||
location = var.region
|
||||
repository_id = var.ar_repo
|
||||
format = "DOCKER"
|
||||
description = "Claude Gateway container images"
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# ── 4 VPC + Private Services Access ──────────────────────────────────────────
|
||||
resource "google_compute_network" "vpc" {
|
||||
project = var.project_id
|
||||
name = var.vpc_network
|
||||
auto_create_subnetworks = false
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "subnet" {
|
||||
project = var.project_id
|
||||
name = var.subnet
|
||||
region = var.region
|
||||
network = google_compute_network.vpc.id
|
||||
ip_cidr_range = var.subnet_range
|
||||
}
|
||||
|
||||
resource "google_compute_global_address" "psa_range" {
|
||||
project = var.project_id
|
||||
name = "google-managed-services-${var.vpc_network}"
|
||||
purpose = "VPC_PEERING"
|
||||
address_type = "INTERNAL"
|
||||
prefix_length = var.psa_prefix_length
|
||||
network = google_compute_network.vpc.id
|
||||
}
|
||||
|
||||
resource "google_service_networking_connection" "psa" {
|
||||
network = google_compute_network.vpc.id
|
||||
service = "servicenetworking.googleapis.com"
|
||||
reserved_peering_ranges = [google_compute_global_address.psa_range.name]
|
||||
# ABANDON: on destroy, leave the producer peering in place (deleting it can hang
|
||||
# and would affect any other private-IP service on this VPC).
|
||||
deletion_policy = "ABANDON"
|
||||
# If the peering already exists (e.g. a previous apply failed partway), patch it
|
||||
# instead of failing the create.
|
||||
update_on_creation_fail = true
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# ── 4 Cloud SQL (private IP only) ───────────────────────────────────────────
|
||||
resource "google_sql_database_instance" "db" {
|
||||
project = var.project_id
|
||||
name = var.db_instance
|
||||
region = var.region
|
||||
database_version = var.db_version
|
||||
deletion_protection = var.deletion_protection
|
||||
depends_on = [google_service_networking_connection.psa]
|
||||
|
||||
settings {
|
||||
tier = var.db_tier
|
||||
ip_configuration {
|
||||
ipv4_enabled = false # private IP only (org policy: sql.restrictPublicIp)
|
||||
private_network = google_compute_network.vpc.id
|
||||
ssl_mode = "ENCRYPTED_ONLY"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_sql_database" "db" {
|
||||
project = var.project_id
|
||||
name = var.db_name
|
||||
instance = google_sql_database_instance.db.name
|
||||
}
|
||||
|
||||
# URL-safe (alphanumeric) so it drops cleanly into the connection string.
|
||||
# nosemgrep: terraform-generic-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "random_password" "db" {
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-gcp-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "google_sql_user" "gateway" {
|
||||
project = var.project_id
|
||||
name = var.db_user
|
||||
instance = google_sql_database_instance.db.name
|
||||
password = random_password.db.result
|
||||
# On destroy the role owns the tables it migrated at boot, so DROP ROLE can
|
||||
# fail (and races google_sql_database.db). ABANDON is harmless on the
|
||||
# greenfield teardown — the whole instance is deleted anyway.
|
||||
deletion_policy = "ABANDON"
|
||||
}
|
||||
|
||||
# ── 5/6 Secrets + secretAccessor ────────────────────────────────────────────
|
||||
# postgres-url: connection string built from the instance's private IP.
|
||||
resource "google_secret_manager_secret" "postgres_url" {
|
||||
project = var.project_id
|
||||
secret_id = var.secret_name
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-gcp-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "google_secret_manager_secret_version" "postgres_url" {
|
||||
secret = google_secret_manager_secret.postgres_url.id
|
||||
secret_data = "postgres://${var.db_user}:${random_password.db.result}@${google_sql_database_instance.db.private_ip_address}:5432/${var.db_name}?sslmode=require"
|
||||
}
|
||||
|
||||
# jwt: session signing key.
|
||||
# nosemgrep: terraform-generic-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "random_password" "jwt" {
|
||||
length = 48
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret" "jwt" {
|
||||
project = var.project_id
|
||||
secret_id = var.jwt_secret_name
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-gcp-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "google_secret_manager_secret_version" "jwt" {
|
||||
secret = google_secret_manager_secret.jwt.id
|
||||
secret_data = random_password.jwt.result
|
||||
}
|
||||
|
||||
# oidc client secret: operator-provided (from the Google OAuth client).
|
||||
resource "google_secret_manager_secret" "oidc" {
|
||||
project = var.project_id
|
||||
secret_id = var.oidc_secret_name
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-gcp-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "google_secret_manager_secret_version" "oidc" {
|
||||
count = var.oidc_client_secret != "" ? 1 : 0
|
||||
secret = google_secret_manager_secret.oidc.id
|
||||
secret_data = var.oidc_client_secret
|
||||
}
|
||||
|
||||
# Warn (not block) at plan time when the OIDC secret value isn't set: the Cloud
|
||||
# Run service mounts gateway-oidc-client-secret:latest unconditionally, so an
|
||||
# empty value with no out-of-band version means the apply fails late at
|
||||
# revision creation. A warning (not a precondition) keeps the documented
|
||||
# out-of-band-version mode usable.
|
||||
check "oidc_client_secret_set" {
|
||||
assert {
|
||||
condition = var.oidc_client_secret != ""
|
||||
error_message = "oidc_client_secret is empty — set it in terraform.tfvars, or add a version to the gateway-oidc-client-secret secret out-of-band before applying (the Cloud Run revision mounts it at :latest and will fail without one)."
|
||||
}
|
||||
}
|
||||
|
||||
# config: gateway.yaml. Guard mirrors the bash REPLACE_ME check (non-comment lines).
|
||||
resource "google_secret_manager_secret" "config" {
|
||||
project = var.project_id
|
||||
secret_id = var.config_secret_name
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-gcp-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote GCS backend (see README "Remote state")
|
||||
resource "google_secret_manager_secret_version" "config" {
|
||||
secret = google_secret_manager_secret.config.id
|
||||
secret_data = local.gateway_config
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = length([
|
||||
for line in split("\n", local.gateway_config) :
|
||||
line
|
||||
if !startswith(trimspace(line), "#") && strcontains(line, "REPLACE_ME")
|
||||
]) == 0
|
||||
error_message = "gateway.yaml still has REPLACE_ME on a non-comment line — fill it in before applying."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "postgres_url" {
|
||||
project = var.project_id
|
||||
secret_id = google_secret_manager_secret.postgres_url.secret_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.gateway.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "jwt" {
|
||||
project = var.project_id
|
||||
secret_id = google_secret_manager_secret.jwt.secret_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.gateway.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "oidc" {
|
||||
project = var.project_id
|
||||
secret_id = google_secret_manager_secret.oidc.secret_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.gateway.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "config" {
|
||||
project = var.project_id
|
||||
secret_id = google_secret_manager_secret.config.secret_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.gateway.email}"
|
||||
}
|
||||
|
||||
# ── 7 Cloud Run (Direct VPC egress) ─────────────────────────────────────────
|
||||
resource "google_cloud_run_v2_service" "gateway" {
|
||||
project = var.project_id
|
||||
name = var.service_name
|
||||
location = var.region
|
||||
ingress = var.ingress
|
||||
invoker_iam_disabled = var.invoker_iam_disabled
|
||||
deletion_protection = var.deletion_protection
|
||||
|
||||
template {
|
||||
service_account = google_service_account.gateway.email
|
||||
scaling {
|
||||
min_instance_count = var.min_instances
|
||||
max_instance_count = var.max_instances
|
||||
}
|
||||
# Secrets are mounted at version=latest, so a config edit or secret
|
||||
# rotation alone wouldn't diff this resource and the warm min_instances=1
|
||||
# revision would keep the old values. Stamping a hash of the rendered
|
||||
# config + every managed secret value forces a new revision whenever any
|
||||
# of them change — without this, tainting random_password.db ALTERs the
|
||||
# SQL role to the new password while the running revision keeps the old
|
||||
# connection string and breaks on its next reconnect, and rotating the
|
||||
# OIDC client secret leaves login failing invalid_client.
|
||||
labels = {
|
||||
config-sha = substr(sha256(join("", [
|
||||
local.gateway_config,
|
||||
random_password.db.result,
|
||||
random_password.jwt.result,
|
||||
var.oidc_client_secret,
|
||||
])), 0, 63)
|
||||
}
|
||||
# Cloud Run's default 300s request timeout would cut off long streaming
|
||||
# /v1/messages responses mid-stream.
|
||||
timeout = "3600s"
|
||||
|
||||
vpc_access {
|
||||
network_interfaces {
|
||||
network = google_compute_network.vpc.id
|
||||
subnetwork = google_compute_subnetwork.subnet.id
|
||||
}
|
||||
egress = "PRIVATE_RANGES_ONLY" # public egress (Agent Platform, accounts.google.com) bypasses the VPC -> no Cloud NAT needed
|
||||
}
|
||||
|
||||
containers {
|
||||
image = local.image
|
||||
ports { container_port = 8080 }
|
||||
|
||||
# gateway.yaml mounted as a file at /etc/claude/gateway.yaml (alone in its dir).
|
||||
volume_mounts {
|
||||
name = "config"
|
||||
mount_path = "/etc/claude"
|
||||
}
|
||||
|
||||
# Cloud Run can't mount multiple secrets in one dir, so the rest are env vars
|
||||
# (gateway.yaml references them via ${ENV_VAR}).
|
||||
env {
|
||||
name = "GATEWAY_JWT_SECRET"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.jwt.secret_id
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "OIDC_CLIENT_SECRET"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.oidc.secret_id
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "GATEWAY_POSTGRES_URL"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.postgres_url.secret_id
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volumes {
|
||||
name = "config"
|
||||
secret {
|
||||
secret = google_secret_manager_secret.config.secret_id
|
||||
items {
|
||||
path = "gateway.yaml"
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
google_secret_manager_secret_iam_member.config,
|
||||
google_secret_manager_secret_iam_member.jwt,
|
||||
google_secret_manager_secret_iam_member.oidc,
|
||||
google_secret_manager_secret_iam_member.postgres_url,
|
||||
google_secret_manager_secret_version.config,
|
||||
google_secret_manager_secret_version.postgres_url,
|
||||
google_secret_manager_secret_version.jwt,
|
||||
google_secret_manager_secret_version.oidc,
|
||||
google_sql_database.db,
|
||||
google_sql_user.gateway,
|
||||
google_project_service.apis,
|
||||
]
|
||||
}
|
||||
|
||||
# Public access at the Cloud Run IAM layer — the gateway runs its own OIDC, so the
|
||||
# invoker check must be opened or disabled (real auth stays the gateway's SSO):
|
||||
# Preferred — disable it: invoker_iam_disabled=true on the service above. No allUsers
|
||||
# binding at all, and it works under Domain Restricted Sharing.
|
||||
# Fallback — open it: this allUsers run.invoker grant. Domain Restricted Sharing orgs
|
||||
# reject allUsers, and an LB does NOT bypass that (ingress is network-layer; the IAM
|
||||
# check still runs) — use invoker_iam_disabled, a DRS exception, or GKE.
|
||||
# Skipped when invoker_iam_disabled=true (the grant would be redundant, and DRS rejects it).
|
||||
resource "google_cloud_run_v2_service_iam_member" "public" {
|
||||
count = var.allow_unauthenticated && !var.invoker_iam_disabled ? 1 : 0
|
||||
project = var.project_id
|
||||
location = var.region
|
||||
name = google_cloud_run_v2_service.gateway.name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
34
examples/gateway/gcp/terraform/outputs.tf
Normal file
34
examples/gateway/gcp/terraform/outputs.tf
Normal 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
|
||||
}
|
||||
26
examples/gateway/gcp/terraform/terraform.tfvars.example
Normal file
26
examples/gateway/gcp/terraform/terraform.tfvars.example
Normal 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-..."
|
||||
184
examples/gateway/gcp/terraform/variables.tf
Normal file
184
examples/gateway/gcp/terraform/variables.tf
Normal 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. (Agent Platform region is set separately inside gateway.yaml.)"
|
||||
type = string
|
||||
default = "us-east5"
|
||||
}
|
||||
|
||||
# ── Service account (§2) ────────────────────────────────────────────────────
|
||||
variable "sa_name" {
|
||||
description = "Service account account_id (the part before @)."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
# ── Image (§3) ──────────────────────────────────────────────────────────────
|
||||
# Terraform creates the Artifact Registry repo but does NOT build/push the image
|
||||
# (that's a docker build step — see README). It references the image by tag.
|
||||
variable "ar_repo" {
|
||||
description = "Artifact Registry Docker repository ID."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
variable "image_name" {
|
||||
description = "Image name within the repo."
|
||||
type = string
|
||||
default = "gateway"
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Image tag — the Claude Code release version you build and push (must already be pushed as linux/amd64). See the README Deploy section for the build command."
|
||||
type = string
|
||||
validation {
|
||||
condition = can(regex("^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$", var.image_tag))
|
||||
error_message = "image_tag must be a valid OCI tag — set it to the Claude Code release version you pushed (the '<version>' in terraform.tfvars.example is a placeholder)."
|
||||
}
|
||||
}
|
||||
|
||||
# ── Networking (§4) ─────────────────────────────────────────────────────────
|
||||
variable "vpc_network" {
|
||||
description = "Custom VPC network name."
|
||||
type = string
|
||||
default = "cc-gateway-vpc"
|
||||
}
|
||||
|
||||
variable "subnet" {
|
||||
description = "Subnet name (Cloud Run Direct VPC egress attaches here)."
|
||||
type = string
|
||||
default = "cc-gateway-subnet"
|
||||
}
|
||||
|
||||
variable "subnet_range" {
|
||||
description = "Subnet primary CIDR."
|
||||
type = string
|
||||
default = "10.0.0.0/24"
|
||||
}
|
||||
|
||||
variable "psa_prefix_length" {
|
||||
description = "Prefix length for the Private Services Access allocated range (/16 is GCP's recommendation)."
|
||||
type = number
|
||||
default = 16
|
||||
}
|
||||
|
||||
# ── Cloud SQL (§4) ──────────────────────────────────────────────────────────
|
||||
variable "db_instance" {
|
||||
description = "Cloud SQL instance name."
|
||||
type = string
|
||||
default = "claude-gateway-db"
|
||||
}
|
||||
|
||||
variable "db_version" {
|
||||
description = "Postgres major version. The gateway supports PostgreSQL 14 or newer; 16 is the recommended default."
|
||||
type = string
|
||||
default = "POSTGRES_16"
|
||||
}
|
||||
|
||||
variable "db_tier" {
|
||||
description = "Cloud SQL machine tier."
|
||||
type = string
|
||||
default = "db-g1-small"
|
||||
}
|
||||
|
||||
variable "db_name" {
|
||||
description = "Database name."
|
||||
type = string
|
||||
default = "claude_gateway"
|
||||
}
|
||||
|
||||
variable "db_user" {
|
||||
description = "Database user (the gateway connects as this role)."
|
||||
type = string
|
||||
default = "gateway"
|
||||
}
|
||||
|
||||
# ── Secrets (§5 / §6) ─────────────────────────────────────────────────────
|
||||
variable "secret_name" {
|
||||
description = "Secret Manager secret holding the Postgres connection string."
|
||||
type = string
|
||||
default = "gateway-postgres-url"
|
||||
}
|
||||
|
||||
variable "jwt_secret_name" {
|
||||
description = "Secret Manager secret holding the session JWT signing key."
|
||||
type = string
|
||||
default = "gateway-jwt-secret"
|
||||
}
|
||||
|
||||
variable "oidc_secret_name" {
|
||||
description = "Secret Manager secret holding the Google OAuth client secret."
|
||||
type = string
|
||||
default = "gateway-oidc-client-secret"
|
||||
}
|
||||
|
||||
variable "config_secret_name" {
|
||||
description = "Secret Manager secret holding gateway.yaml (mounted at /etc/claude/gateway.yaml)."
|
||||
type = string
|
||||
default = "gateway-config"
|
||||
}
|
||||
|
||||
variable "oidc_client_secret" {
|
||||
description = "Google OAuth client secret value. Leave empty to NOT manage the version via Terraform (only if you add the secret version out-of-band — without one the deploy fails)."
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "gateway_config_path" {
|
||||
description = "Path to gateway.yaml. Empty = ../gateway.yaml relative to this module."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ── Cloud Run (§7) ──────────────────────────────────────────────────────────
|
||||
variable "service_name" {
|
||||
description = "Cloud Run service name."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
variable "min_instances" {
|
||||
description = "Minimum Cloud Run instances (1 avoids cold OIDC discovery)."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "max_instances" {
|
||||
description = "Maximum Cloud Run instances. Each instance opens a Postgres pool of up to 5 connections (the gateway's store.max_connections default) and db-g1-small caps at ~50 max_connections — keep max_instances × 5 below the DB tier's limit, or raise the tier before raising this."
|
||||
type = number
|
||||
default = 8
|
||||
}
|
||||
|
||||
variable "ingress" {
|
||||
description = "Cloud Run ingress — Claude Code's /login only accepts gateway hosts on private addresses, so public ingress cannot serve clients: INGRESS_TRAFFIC_INTERNAL_ONLY (default; no public URL — VPC-only; reaches corp on-prem only with the private-access prerequisites in the README; public_url stays the run.app URL, so no LB or custom cert needed) or INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER (front with your own internal ALB for a custom hostname/cert)."
|
||||
type = string
|
||||
default = "INGRESS_TRAFFIC_INTERNAL_ONLY"
|
||||
validation {
|
||||
condition = contains(["INGRESS_TRAFFIC_INTERNAL_ONLY", "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"], var.ingress)
|
||||
error_message = "ingress must be INGRESS_TRAFFIC_INTERNAL_ONLY or INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER — Claude Code only connects to gateways on private addresses."
|
||||
}
|
||||
}
|
||||
|
||||
variable "invoker_iam_disabled" {
|
||||
description = "PREFERRED public-access path: disable the Cloud Run invoker IAM check so requests reach the container with no allUsers binding (works under Domain Restricted Sharing). Real auth stays the gateway's own OIDC. When true, the allUsers grant below is skipped. May be blocked by org policy constraints/run.managed.requireInvokerIam, or unavailable for the org (\"invoker_iam_disabled is not currently available for your organization\") — then fall back to allow_unauthenticated. Requires google provider >= 6.8."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "allow_unauthenticated" {
|
||||
description = "Fallback public-access path: grant allUsers run.invoker (the gateway needs the IAM layer open for its own OIDC). Prefer invoker_iam_disabled. Domain Restricted Sharing orgs reject allUsers — set false there and use invoker_iam_disabled, a DRS exception, or GKE."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "deletion_protection" {
|
||||
description = "Provider-level deletion protection on Cloud SQL and Cloud Run. Keep true to avoid accidental deletion of the running deployment."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
19
examples/gateway/gcp/terraform/versions.tf
Normal file
19
examples/gateway/gcp/terraform/versions.tf
Normal 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
|
||||
}
|
||||
229
feed.xml
229
feed.xml
@@ -6,7 +6,139 @@
|
||||
<author><name>Anthropic</name></author>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md"/>
|
||||
<link rel="self" type="application/atom+xml" href="https://raw.githubusercontent.com/anthropics/claude-code/main/feed.xml"/>
|
||||
<updated>2026-06-26T21:29:36Z</updated>
|
||||
<updated>2026-07-03T16:52:26Z</updated>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.200</id>
|
||||
<title>Claude Code v2.1.200</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.200"/>
|
||||
<updated>2026-07-03T16:52:26Z</updated>
|
||||
<content type="html"><p>• Changed AskUserQuestion dialogs to no longer auto-continue by default; opt into an idle timeout via /config</p>
|
||||
<p>• Changed the "default" permission mode to "Manual" across the CLI, --help, VS Code, and JetBrains; --permission-mode manual and "defaultMode": "manual" are accepted alongside default</p>
|
||||
<p>• Fixed a crash at startup when disabledMcpServers or enabledMcpServers in .claude.json is set to a non-array value</p>
|
||||
<p>• Fixed background sessions silently stopping mid-turn after sleep/wake or when reopening a stalled session</p>
|
||||
<p>• Fixed background sessions re-running a turn cancelled with Esc after a stall respawn</p>
|
||||
<p>• Fixed background agents never starting again after a crash left a stale daemon.lock whose PID the OS reused</p>
|
||||
<p>• Fixed background-agent daemon handover so a reinstalled older build can no longer take over the daemon; build recency is now judged by the version's embedded build timestamp</p>
|
||||
<p>• Fixed background-agent roster issues: transient corruption permanently disabling orphan cleanup, older binaries not preserving fields written by newer versions, and socket auth tokens being stripped during daemon restarts</p>
|
||||
<p>• Fixed subagents cut off by a rate limit before producing any text output returning an empty result instead of failing cleanly</p>
|
||||
<p>• Fixed control bytes from background-agent output reaching the terminal in the agent view</p>
|
||||
<p>• Fixed claude agents --plugin-dir &lt;dir&gt; not showing the plugin's agents and skills in the agent view when the flag is placed after agents</p>
|
||||
<p>• Fixed project-scoped plugins not loading correctly from git worktrees of the same repository</p>
|
||||
<p>• Fixed /mcp server list not tracking focus for screen readers and magnifiers</p>
|
||||
<p>• Fixed voice dictation showing a misleading "Voice connection failed" message when a recording captures no audio</p>
|
||||
<p>• Fixed rendering flicker under tmux 3.4+ by enabling synchronized terminal output</p>
|
||||
<p>• Improved screen-reader output: decorative glyphs are now hidden, transcript symbols read as short labels, and nested tables read as Header: value. lines</p>
|
||||
<p>• Improved the install script to explain when installation is killed by the system running out of memory</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.199</id>
|
||||
<title>Claude Code v2.1.199</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.199"/>
|
||||
<updated>2026-07-02T23:35:12Z</updated>
|
||||
<content type="html"><p>• Stacked slash-skill invocations like /skill-a /skill-b do XYZ now load all leading skills (up to 5), not just the first</p>
|
||||
<p>• Fixed SSL certificate errors (TLS-inspecting proxies, missing NODE_EXTRA_CA_CERTS, expired certs) burning retries before showing actionable guidance — they now fail immediately with the fix hint</p>
|
||||
<p>• Fixed streaming responses being discarded when the API emits a mid-stream overloaded/server error after partial output — the partial is now kept with an incomplete-response notice</p>
|
||||
<p>• Fixed subagents cut off by a rate limit or server error silently failing instead of returning their partial work to the parent</p>
|
||||
<p>• Fixed subagents reporting API errors (e.g. usage limit reached) as successful results — the error is now reported to the parent agent</p>
|
||||
<p>• Fixed the background-agent daemon on Linux killing itself and every running agent every ~50 seconds after an unclean shutdown left a corrupted worker record</p>
|
||||
<p>• Fixed background agents failing to cold-start over SSH on macOS with "Could not switch to audit session" (regression in 2.1.196)</p>
|
||||
<p>• Fixed claude stop being silently undone when it raced a background-agent respawn — the respawn now honors the stop</p>
|
||||
<p>• Fixed background job progress indicators stalling for minutes while the job ran long commands</p>
|
||||
<p>• Fixed background sessions on memory-starved machines showing a generic error — they now indicate low memory and suggest freeing resources</p>
|
||||
<p>• Fixed remote sessions briefly flapping between Working and Idle in the agent view when a background agent completes</p>
|
||||
<p>• Fixed idle subagents vanishing from the agent panel while other subagents were still working; surplus idle agents now collapse into an expandable summary row</p>
|
||||
<p>• Fixed typing /model or /fast while viewing a subagent silently opening the lead's model picker — a notice now explains the command applies to the lead</p>
|
||||
<p>• Fixed SessionStart, Setup, and SubagentStart hooks silently hiding stderr when exiting with code 2 — the error is now shown in the transcript</p>
|
||||
<p>• Fixed claude --dangerously-skip-permissions daemon &lt;subcommand&gt; being treated as a chat prompt instead of running the subcommand</p>
|
||||
<p>• Fixed SendMessage silently misrouting when a re-spawned agent reuses a previous agent's name — the tool now detects the mismatch and asks the caller to retarget</p>
|
||||
<p>• Fixed opening or resuming a session with no new messages needlessly growing the transcript file</p>
|
||||
<p>• Fixed backgrounding a session with ← or /background dropping its /color from the agent view row</p>
|
||||
<p>• Fixed resetting a corrupted config file from the startup recovery dialog destroying it unrecoverably — it now backs up the file first</p>
|
||||
<p>• Fixed Claude in Chrome repeatedly opening the reconnect page when sessions run from different builds or config directories</p>
|
||||
<p>• Fixed plan mode not prompting for state-changing browser tool calls; read-only browser_batch calls are now correctly auto-allowed</p>
|
||||
<p>• Transient server rate-limit errors (429s unrelated to your usage limit) are now retried automatically with backoff for subscribers instead of failing the turn</p>
|
||||
<p>• CLAUDE_CODE_RETRY_WATCHDOG now raises the default retry count for non-capacity transient errors to 300 and lifts the cap of 15 on CLAUDE_CODE_MAX_RETRIES</p>
|
||||
<p>• claude agents session rows now show pull-request links as bare #N without the redundant "PR" label</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.198</id>
|
||||
<title>Claude Code v2.1.198</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.198"/>
|
||||
<updated>2026-07-01T20:45:29Z</updated>
|
||||
<content type="html"><p>• Subagents now run in the background by default, so Claude keeps working while they run and is notified when they finish (previously a gradual rollout)</p>
|
||||
<p>• Claude in Chrome is now generally available</p>
|
||||
<p>• Added background agent notifications in claude agents — sessions that need input or finish now fire the Notification hook (agent_needs_input / agent_completed)</p>
|
||||
<p>• Added /dataviz skill for chart and dashboard design guidance with a runnable color-palette validator</p>
|
||||
<p>• Gateway: added Claude Platform on AWS (anthropicAws) as an upstream provider; model-not-found responses now advance the failover chain</p>
|
||||
<p>• Background agents launched from claude agents now commit, push, and open a draft PR when they finish code work in a worktree, instead of stopping to ask</p>
|
||||
<p>• The built-in Explore agent now inherits the main session's model (capped at opus) instead of running on haiku</p>
|
||||
<p>• Subagents and context compaction now inherit the session's extended thinking configuration, improving output quality on delegated tasks</p>
|
||||
<p>• Fixed brief network drops mid-response aborting the turn — transient errors like ECONNRESET now retry with backoff instead of failing</p>
|
||||
<p>• Fixed excessive background classifier requests when sandboxed processes repeatedly accessed the same network host</p>
|
||||
<p>• Fixed background tasks in web, desktop, and VS Code task panels getting stuck on "Running" after they finish or after resuming a session</p>
|
||||
<p>• Fixed agent teams: a teammate that dies on an API error now reports "failed" to the lead, and messaging a stuck teammate wakes it to retry immediately</p>
|
||||
<p>• Fixed the /diff panel not refreshing when you switch branches or commit outside the session</p>
|
||||
<p>• Fixed markdown tables overflowing and wrapping their right border when rendered in fullscreen mode</p>
|
||||
<p>• Fixed Claude Platform on AWS and Mantle sessions dead-ending with "Please run /login" when the STS token expires — awsAuthRefresh now runs automatically</p>
|
||||
<p>• Fixed "no route to host" for local-network hosts in macOS background agent sessions by declaring Local Network entitlements</p>
|
||||
<p>• Fixed /desktop failing with "Cannot determine working directory" after entering and exiting a worktree</p>
|
||||
<p>• Fixed background agents repeatedly showing "Reconnecting…" every ~52 seconds on macOS while the agents view was open</p>
|
||||
<p>• Fixed pressing ← inside claude attach &lt;id&gt; exiting to the shell instead of opening the agent view</p>
|
||||
<p>• Fixed claude --bg silently creating an unattachable session when combined with --print/-p; the conflicting flags are now rejected up front</p>
|
||||
<p>• Fixed the workflow progress view dropping the earliest agents from the list while the phase counter stayed correct in SDK and desktop-app sessions</p>
|
||||
<p>• Fixed .claude/rules/ conditional rules not loading when the target file is reached via a symlinked path</p>
|
||||
<p>• Fixed Cmd+click not opening URLs in fullscreen mode in Warp on macOS</p>
|
||||
<p>• Fixed double-click word selection in fullscreen mode to select the entire URL including the scheme</p>
|
||||
<p>• Fixed plan mode not auto-allowing read-only tool calls when a session starts in plan mode</p>
|
||||
<p>• Fixed /branch deriving its default fork name from the compaction summary instead of the first real prompt</p>
|
||||
<p>• Improved focus mode: subagents launched in a turn now appear in its activity summary, and completed background notifications fold into a single count</p>
|
||||
<p>• Improved syntax highlighting accuracy in code blocks, diffs, and file previews by upgrading to highlight.js 11</p>
|
||||
<p>• Keyboard shortcut hints now show opt/cmd instead of alt/super when connected from a Mac over SSH</p>
|
||||
<p>• Improved API retry UX: the error reason is now shown after the second attempt, and a status page link replaces the spinner tip when the API is overloaded</p>
|
||||
<p>• /login now opens the sign-in dialog from the claude agents view instead of saying it isn't available</p>
|
||||
<p>• Subagents now treat messages from the agent that launched them as normal task direction; an agent's message is still never treated as the user's approval</p>
|
||||
<p>• Removed the /agents wizard; ask Claude to create or manage subagents, or edit .claude/agents/ directly</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.197</id>
|
||||
<title>Claude Code v2.1.197</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.197"/>
|
||||
<updated>2026-06-30T17:56:29Z</updated>
|
||||
<content type="html"><p>• Introducing Claude Sonnet 5: now the default model in Claude Code, with a native 1M-token context window and promotional pricing of $2/$10 per Mtok through August 31. Update to version 2.1.197 for access. https://www.anthropic.com/news/claude-sonnet-5</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.196</id>
|
||||
<title>Claude Code v2.1.196</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.196"/>
|
||||
<updated>2026-06-29T23:27:24Z</updated>
|
||||
<content type="html"><p>• Added support for organization default models — admins set it in the org console; it shows as "Org default" (or "Role default") in /model when you haven't picked one yourself</p>
|
||||
<p>• Added readable default names for sessions at start, making them easier to identify and message</p>
|
||||
<p>• Added clickable file attachments in chat — Cmd/Ctrl-click reveals the file in Finder/Explorer</p>
|
||||
<p>• Security: claude mcp list/get no longer spawn .mcp.json servers that a repo self-approved via a committed .claude/settings.json; untrusted workspaces show ⏸ Pending approval</p>
|
||||
<p>• Fixed waking a background job permanently deleting its conversation and re-running the original prompt when the transcript probe misread a real transcript; the file is now set aside, never deleted</p>
|
||||
<p>• Fixed the rate-limit warning flickering off and rate-limit telemetry being over-counted when multiple parallel requests were in flight at the moment a usage limit was hit</p>
|
||||
<p>• Fixed duplicate recap lines after a background session's turn: a schema-rejected StructuredOutput attempt no longer renders alongside its retry</p>
|
||||
<p>• Fixed PowerShell git diff/git grep, egrep/fgrep, and quoted search patterns containing | being reported as failures when they exit 1, matching Bash behavior</p>
|
||||
<p>• Fixed multiple claude agents side panel issues: keyboard focus getting stuck when opening an agent, background jobs losing their subagent types on every open, and sessions showing incorrect status while actively running</p>
|
||||
<p>• Fixed claude agents --dangerously-skip-permissions silently falling back to auto mode instead of showing the bypass disclaimer and applying bypass mode to spawned agents</p>
|
||||
<p>• Fixed mid-turn crash recovery for Remote sessions — sessions interrupted by a server restart now auto-resume on the next worker</p>
|
||||
<p>• Fixed sessions moved with /cd reappearing in the old directory's resume list after a non-graceful exit when the old path contained special characters</p>
|
||||
<p>• Fixed claude plugin validate skipping local plugins whose source is "." and stopping after the first error class</p>
|
||||
<p>• Fixed Esc Esc at an idle prompt not opening the rewind menu (regression); use Ctrl+C or Ctrl+X Ctrl+K to stop background agents</p>
|
||||
<p>• Fixed MCP OAuth requesting the authorization server's full scopes_supported catalog when no scope is specified, causing invalid_scope failures on GitLab self-hosted and other enterprise IdPs</p>
|
||||
<p>• Fixed /context showing 0 tokens for all tool groups on Bedrock</p>
|
||||
<p>• Fixed /deep-research misreporting verifier failures as "all claims refuted" instead of unverified</p>
|
||||
<p>• Fixed plugin dependency version pins not being honored when the marketplace was added as a local folder path backed by a git repo</p>
|
||||
<p>• Fixed claude agents session status: completed rows no longer flip between "Done" and "Needs your input", stalled agents are now labeled "Needs attention", and results that mention a PR show a clickable link</p>
|
||||
<p>• Fixed voice dictation swallowing spaces and spuriously starting a recording during very fast typing when voice mode is enabled</p>
|
||||
<p>• Improved background session reliability: long-running commands and workflows now survive the session's process being stopped, restarted, or updated — including on Windows, where background shells are handed off instead of being killed</p>
|
||||
<p>• Improved background agents: workers killed by a daemon restart are now automatically resumed from where they left off the next time the agents view opens</p>
|
||||
<p>• Improved /code-review workflow: merged five cleanup finders into one, cutting token usage by roughly 25%</p>
|
||||
<p>• Reduced per-frame rendering work in the terminal UI by skipping no-op subtree walks during streaming</p>
|
||||
<p>• The streaming idle watchdog is now on by default for all providers — it aborts and retries when a response stream produces no events for 5 minutes. Set CLAUDE_ENABLE_STREAM_WATCHDOG=0 to disable.</p>
|
||||
<p>• Remote Control is now disabled when ANTHROPIC_BASE_URL points at a non-Anthropic host, matching the existing behavior under CLAUDE_CODE_USE_BEDROCK/_VERTEX/_FOUNDRY</p>
|
||||
<p>• Changed opening the agents view from a foreground session to require a single ← press instead of two, matching the behavior in background sessions</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.195</id>
|
||||
<title>Claude Code v2.1.195</title>
|
||||
@@ -327,99 +459,4 @@
|
||||
<content type="html"><p>• 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</p>
|
||||
<p>• Fixed a spurious "sandbox dependencies missing" startup warning on Windows when sandbox was enabled in settings</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.172</id>
|
||||
<title>Claude Code v2.1.172</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.172"/>
|
||||
<updated>2026-06-10T20:44:09Z</updated>
|
||||
<content type="html"><p>• Sub-agents can now spawn their own sub-agents (up to 5 levels deep)</p>
|
||||
<p>• 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</p>
|
||||
<p>• Added a search bar when browsing a marketplace's plugins in /plugin</p>
|
||||
<p>• Added model attribute to the claude_code.lines_of_code.count OTEL metric</p>
|
||||
<p>• Fixed sessions using 1M context without usage credits getting permanently stuck — the session now automatically compacts back under the standard context limit</p>
|
||||
<p>• Fixed a repeating "an image in the conversation could not be processed and was removed" error when the conversation contained multiple images</p>
|
||||
<p>• Fixed the agents view keeping a session under Working with a busy spinner for up to 30 seconds after the worker replied</p>
|
||||
<p>• Fixed background agents potentially reading another directory's project settings (.mcp.json approvals, trust) when dispatched onto a pre-warmed worker</p>
|
||||
<p>• Fixed background-session attach failing with EAUTH for sessions started on an older version after the daemon auto-updated</p>
|
||||
<p>• Fixed a background sub-agent staying stuck as "active" in the agent panel after a nested agent it spawned was stopped</p>
|
||||
<p>• Fixed /model suggestions in the claude agents dispatch input rendering with a misleading slash prefix and showing models disabled for your org</p>
|
||||
<p>• Fixed availableModels restrictions not being applied to subagent model overrides, the agent dispatch model picker, and the advisor model</p>
|
||||
<p>• Fixed availableModels allowlists hiding the /model picker's Opus and Sonnet 1M rows when entries use version-specific IDs like claude-opus-4-8</p>
|
||||
<p>• 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</p>
|
||||
<p>• Fixed model IDs getting a doubled 1M-context suffix (e.g. [1M][1m]) when ANTHROPIC_DEFAULT_OPUS_MODEL already includes one</p>
|
||||
<p>• 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</p>
|
||||
<p>• 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</p>
|
||||
<p>• Fixed up-arrow prompt history showing the main agent's prompts while a subagent's chat tab is open</p>
|
||||
<p>• Fixed memory recall not finding mounted team memory stores (CLAUDE_MEMORY_STORES) in remote sessions</p>
|
||||
<p>• Fixed workflow validation rejecting scripts whose prompt strings or comments merely mention Date.now()/Math.random()</p>
|
||||
<p>• Disable mouse tracking on Windows consoles that don't fully support it</p>
|
||||
<p>• 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</p>
|
||||
<p>• Improved performance in long conversations by removing redundant message normalization and avoiding full message-history transforms when streaming tool-use state is unchanged</p>
|
||||
<p>• 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</p>
|
||||
<p>• Improved Claude in Chrome tool loading: browser tools now load in a single batched call instead of one per tool</p>
|
||||
<p>• Improved the non-interactive Usage Policy refusal message to suggest starting a new session or changing your model</p>
|
||||
<p>• /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</p>
|
||||
<p>• Shortened the Remote Control footer indicator to "/rc active" and hid it on narrow terminals</p>
|
||||
<p>• Stopped promoting /loop in remote sessions, where pending loops don't keep the container alive</p>
|
||||
<p>• [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</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.170</id>
|
||||
<title>Claude Code v2.1.170</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.170"/>
|
||||
<updated>2026-06-09T17:23:03Z</updated>
|
||||
<content type="html"><p>• Introducing Claude Fable 5: a Mythos-class model that we’ve made safe for general use. Fable’s capabilities exceed those of any model we’ve ever made generally available. Update to version 2.1.170 for access. https://www.anthropic.com/news/claude-fable-5-mythos-5</p>
|
||||
<p>• 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.</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.169</id>
|
||||
<title>Claude Code v2.1.169</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.169"/>
|
||||
<updated>2026-06-08T21:57:10Z</updated>
|
||||
<content type="html"><p>• 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)</p>
|
||||
<p>• 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</p>
|
||||
<p>• Added /cd command to move a session to a new working directory without breaking the prompt cache mid-session</p>
|
||||
<p>• 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</p>
|
||||
<p>• 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</p>
|
||||
<p>• 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</p>
|
||||
<p>• Fixed a ~30-50ms UI stall at the start of each turn for macOS users logged in with claude.ai credentials</p>
|
||||
<p>• Fixed claude -p being slow or appearing to hang on Windows while waiting for the slash-command/skill scan (regression in 2.1.161)</p>
|
||||
<p>• Fixed Remote Control getting stuck on "reconnecting" after resuming a session when an OAuth token refresh happened at the same time</p>
|
||||
<p>• Fixed Git Credential Manager's "Connect to GitHub" popup appearing on Windows at startup when background git commands ran without cached credentials</p>
|
||||
<p>• Fixed footer hints (e.g. "esc to interrupt") not showing for users with a custom statusline</p>
|
||||
<p>• Fixed stale permission and dialog prompts reappearing every time you reattached to a remote session whose worker had died while waiting on them</p>
|
||||
<p>• Fixed claude agents --json omitting blocked and just-dispatched background sessions; added --all to include completed sessions, plus new id and state fields</p>
|
||||
<p>• Fixed agents view leaving a stale/garbled frame after navigating back from an agent on WSL in Windows Terminal</p>
|
||||
<p>• Fixed background agents ignoring project-level settings env values (e.g. ANTHROPIC_MODEL) when dispatched onto a pre-warmed worker</p>
|
||||
<p>• Fixed MCPB plugin cache being spuriously invalidated on Windows, causing unnecessary re-extraction</p>
|
||||
<p>• Fixed plugin .in_use PID lock files accumulating without bound; stale markers from crashed sessions are now swept once per day</p>
|
||||
<p>• Fixed untrusted project settings being able to set OTEL client-certificate paths without trust confirmation</p>
|
||||
<p>• /workflows now opens immediately even while a turn is in progress</p>
|
||||
<p>• Improved TaskCreate reliability: malformed inputs are repaired automatically and validation errors for unloaded tools include the schema</p>
|
||||
<p>• Improved the error message shown when your organization has disabled API key authentication, with guidance based on where the active API key comes from</p>
|
||||
<p>• Reduced CPU usage while responses stream and during spinner animations</p>
|
||||
<p>• 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</p>
|
||||
<p>• 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</p>
|
||||
<p>• Background sessions now preserve --ide, --chrome, --bare, --remote-control, and other flags across retire→wake, and respawn state validation was hardened</p>
|
||||
<p>• Background sessions are now told that shared-checkout edits are blocked until they enter a worktree, avoiding a wasted rejected edit before EnterWorktree</p>
|
||||
<p>• The "CLAUDE.md is too long" warning threshold now scales with the model's context window</p>
|
||||
<p>• Auto-updater on Windows now stops retrying within a session once claude.exe is held by another process</p>
|
||||
<p>• Improved color contrast for skill tags in the slash-command menu</p>
|
||||
<p>• Promo credit claims for Apple/Google-billed subscribers without a payment method now explain where to add one</p>
|
||||
<p>• Added a tip suggesting claude agents when running multiple concurrent sessions</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.168</id>
|
||||
<title>Claude Code v2.1.168</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.168"/>
|
||||
<updated>2026-06-06T23:41:47Z</updated>
|
||||
<content type="html"><p>• Bug fixes and reliability improvements</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.167</id>
|
||||
<title>Claude Code v2.1.167</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.167"/>
|
||||
<updated>2026-06-06T01:33:29Z</updated>
|
||||
<content type="html"><p>• Bug fixes and reliability improvements</p></content>
|
||||
</entry>
|
||||
</feed>
|
||||
|
||||
Reference in New Issue
Block a user