mirror of
https://github.com/anthropics/claude-code.git
synced 2026-07-22 03:43:29 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac062f33ab | ||
|
|
c4dbd740a7 | ||
|
|
843297f6b1 | ||
|
|
b799fcaf9f | ||
|
|
4d07874235 | ||
|
|
015170d3fd | ||
|
|
07dcb0e135 | ||
|
|
67f390c9a0 | ||
|
|
c39cb0f14b | ||
|
|
b7784f2c63 | ||
|
|
c9181ca6eb |
250
CHANGELOG.md
250
CHANGELOG.md
@@ -1,5 +1,252 @@
|
||||
# Changelog
|
||||
|
||||
## 2.1.217
|
||||
|
||||
- Added emoji shortcode autocomplete in the prompt input: type `:heart:` to insert ❤️, or `:hea` for suggestions — disable with the `emojiCompletionEnabled` setting
|
||||
- Added warnings when transcript writes are failing (e.g. disk full) or when session saving is off due to an inherited environment variable, instead of losing transcripts silently
|
||||
- Fixed a memory leak where truncated MCP tool outputs kept the full untruncated result in memory for the rest of the session
|
||||
- Fixed Windows auto-update failures that could leave `claude.exe` missing; failed updates now restore the preserved executable automatically
|
||||
- Fixed background session isolation not canonicalizing symlinked working directories, which could let sessions escape their workspace folder
|
||||
- Fixed auto-compact never triggering for Claude Opus 4.8 on Bedrock and `/compact` failing once over the limit
|
||||
- Fixed corporate mTLS, TLS-verify, OAuth scope, and proxy settings being ignored in Claude Desktop sessions
|
||||
- Fixed screen reader mode's startup announcement being cut off by the first prompt render, and the thinking status row re-rendering every few seconds to update elapsed time and token counts
|
||||
- Fixed managed settings that set `OTEL_EXPORTER_OTLP_ENDPOINT` not governing all signals — lower-scope signal-specific overrides no longer redirect telemetry away from the managed endpoint
|
||||
- Fixed `--resume`/`--continue` and `/resume` failing with a TypeError when a transcript has a malformed attachment entry
|
||||
- Fixed Remote Control sessions not showing a pending permission prompt or dialog to viewers that connected after it appeared
|
||||
- Fixed background shells sometimes becoming impossible to stop after a session is sent to the background (`/background` or `←`) or when the session exits on a heavily loaded machine, most visible on Windows
|
||||
- Fixed a `CLAUDE.md` or `SKILL.md` paths frontmatter value with many brace groups OOM-killing or stalling the CLI at startup — brace expansion is now budget-bounded
|
||||
- Fixed the transcript preview sitting flush against the input area when attaching to a starting background session; it now leaves the same one-line gap as the live layout, so the transcript no longer shifts when the session takes over
|
||||
- Improved footer PR badge links to be clickable hyperlinks even when terminal support can't be detected (e.g. over ssh/tmux); set `FORCE_HYPERLINK=0` to opt out
|
||||
- Changed the login-expiry warning to appear 3 days before expiry instead of 5
|
||||
- Capped the frontend-design plugin suggestion tip at 3 lifetime impressions instead of repeating indefinitely
|
||||
- Added a cap on concurrently-running subagents (default 20, override with `CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS`) so one message can't fan out unbounded background agents
|
||||
- Changed subagents to no longer spawn nested subagents by default; set `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` to allow deeper nesting
|
||||
- Fixed `--max-budget-usd` not stopping background subagents: once the cap is reached, new spawns are denied and running background agents are halted
|
||||
|
||||
## 2.1.216
|
||||
|
||||
- Added `sandbox.filesystem.disabled` setting to skip filesystem isolation while keeping network egress control
|
||||
- Fixed a slowdown in long sessions where message normalization cost grew quadratically with the number of turns, causing multi-second stalls and slow resumes
|
||||
- Fixed auto mode denying commands with "HTTP 401" classifier errors after the OAuth token expired or rotated mid-session
|
||||
- Fixed AskUserQuestion telling Claude to continue even when your answer asked it to wait or explain first — free-text answers now get neutral wording
|
||||
- Fixed Claude Code on the web re-asking the same question and dropping your answer after the session sat idle for a few minutes
|
||||
- Fixed @-mentions silently attaching nothing after file-modifying hooks, vim dot-repeat of `c`-operators and paste, statusline running twice on resume, and resume-picker hangs on failure
|
||||
- Fixed resumed background agent sessions reverting to the default agent: the agent's prompt and tool restrictions are now restored
|
||||
- Fixed worktree-isolated subagents redirecting git into the shared checkout via `git -C`, `--git-dir`, or `GIT_DIR`/`GIT_WORK_TREE`
|
||||
- Fixed worktree sessions landing in another project's leftover worktree when the working directory did not match the selected project
|
||||
- Fixed background sessions whose worktree has no git repository being undeletable
|
||||
- Fixed `claude daemon stop --any` potentially terminating an unrelated process via a stale legacy daemon lockfile
|
||||
- Fixed Esc-Esc at an idle prompt not opening the rewind picker in long-running sessions with background tasks
|
||||
- Fixed Bash command permission checking for compound statements with redirects inside `&&` lists or negations
|
||||
- Fixed pressing Ctrl+X twice in the agent list failing to delete a session, and deleted sessions reappearing when their background worker had died
|
||||
- Fixed background subagents getting cancelled when a high-priority message arrives during their startup window
|
||||
- Fixed mouse and focus garbage in the terminal while a GUI editor from `/memory`, `/plan`, `/keybindings`, or Ctrl+G is open; `/memory` no longer waits for the editor to close
|
||||
- Fixed Claude-in-Chrome 403-looping on reconnect when the session's OAuth token lacks a required scope
|
||||
- Fixed workflow saves and scheduled-task writes following a symlink at `.claude`, which could redirect writes outside the project
|
||||
- Fixed MCP re-authenticate revoking working credentials before the new sign-in succeeds, and the reconnect needs-auth message in background sessions pointing at an unusable command
|
||||
- Fixed read-only commands on Windows accessing network paths without a permission prompt
|
||||
- Fixed Bash command parsing of non-ASCII characters to match real shell word boundaries
|
||||
- Fixed PowerShell tool permission validation of commands containing invisible Unicode characters
|
||||
- Fixed dialogs in fullscreen mode stretching past the right-hand edge of their panel
|
||||
- Fixed the `/config` settings list in fullscreen mode clipping its keyboard-hint footer
|
||||
- Fixed the transcript-mode (Ctrl+O) footer hint wrapping on terminals narrower than 104 columns
|
||||
- Fixed the Prometheus metrics endpoint (`OTEL_METRICS_EXPORTER=prometheus`) emitting invalid `# UNIT` lines
|
||||
- Fixed skills and commands changed during a session not appearing in the slash menu until restart
|
||||
- Fixed plugin skills with a `name` frontmatter field losing their plugin prefix in slash-command autocomplete
|
||||
- Fixed telemetry misreporting permission denials: failed permission-prompt requests no longer count as user rejections, and user interrupts are now reported as user aborts instead of rejections
|
||||
- Improved the `/fork` confirmation to one line with the new session's name, `claude attach` id, and a note when the copy shares your checkout
|
||||
- Improved validation of `git` and `gh` command arguments in the PowerShell tool
|
||||
- Improved the `/ultrareview` diff-too-large error to show configured limits, measured diff size, and largest contributing files
|
||||
- Improved `/code-review ultra` empty-diff message to name the exact base ref and suggest passing an explicit base
|
||||
- Improved the spend limit adjustment prompt to show the server's reason when a spend limit change is rejected
|
||||
- `/context` now shows an explicit warning when the conversation exceeds the context window, and a failed `/compact` displays as an error
|
||||
- `/rewind` no longer restores or deletes files through symlinks or hard links at tracked paths and reports how many paths it skipped
|
||||
- Background sessions: `/mcp` and `/install-github-app` now park a "needs input" request in the agent view when no client is attached
|
||||
- Updated the bundled dataviz skill: reordered the default chart palette and fixed guidance that suggested direct labels for four-series charts
|
||||
- [VSCode] Fixed right-to-left text (Arabic, Hebrew, Persian) rendering in the wrong order when mixed with English or code
|
||||
- Fixed cloud sessions dropping the in-flight message when the session's container restarts mid-turn — the interrupted turn now re-runs on resume instead of leaving the session unresponsive
|
||||
|
||||
## 2.1.215
|
||||
|
||||
- Claude no longer runs the `/verify` and `/code-review` skills on its own; invoke them with `/verify` or `/code-review` when you want them
|
||||
|
||||
## 2.1.214
|
||||
|
||||
- Fixed single-segment `dir/**` allow rules like `Edit(src/**)` auto-approving writes to nested `dir/` directories anywhere in the tree instead of only `<cwd>/dir`
|
||||
- Fixed a permission-check bypass affecting commands run in Windows PowerShell 5.1 sessions
|
||||
- Fixed Bash permission checks to fail closed on file-descriptor redirect forms that bash parses differently than the permission analyzer
|
||||
- Fixed Bash permission checks misjudging very long commands — commands over 10,000 characters now always prompt instead of running automatically
|
||||
- Fixed Bash permission checks treating zsh variable subscripts and modifiers in `[[ ]]` comparisons as inert text — these commands now prompt for approval
|
||||
- Fixed Bash permission checks to no longer auto-approve certain `help` and `man` commands that could run unsafe options, command substitutions, or backslash paths
|
||||
- Fixed permission prompts on remote sessions that could proceed before the local confirmation dialog
|
||||
- Added the EndConversation tool: Claude can end sessions with highly abusive users or jailbreak attempts, as on claude.ai since 2025 — see https://www.anthropic.com/research/end-subset-conversations
|
||||
- Added a periodic progress heartbeat for long-running tool calls that previously went silent
|
||||
- Added an ISO `modified` timestamp to memory file frontmatter
|
||||
- Added `message.uuid`, `client_request_id`, and `tool_source` attributes to OpenTelemetry log events for message-level correlation and tool provenance
|
||||
- Added `CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH` to configure the 60 KB truncation limit on OpenTelemetry content attributes
|
||||
- Added reasoning effort to the `subagentStatusLine` payload, so custom agent rows can render model and effort
|
||||
- Added permission prompts for `docker` commands (including the Podman `docker` shim) carrying daemon-redirect flags (`--url`, `--connection`, `--identity`, and Podman's remote mode) that previously ran without one
|
||||
- Fixed a crash when a GrowthBook feature evaluates to null, and a bug where a malformed flag payload could wipe the cached feature flags
|
||||
- Fixed Bash tool killing the Claude session when a `pkill -f` pattern accidentally matched the CLI's own process (Linux)
|
||||
- Fixed unbounded memory growth when `--settings` points at a device file or multi-GB file; oversized (>2 MiB) settings files now fail at startup with a clear error
|
||||
- Fixed streaming turns failing with "Socket is closed" behind corporate proxies on Windows
|
||||
- Fixed stream-json output truncation at exit for slow-reading SDK/pipeline consumers; the exit drain now scales with queued bytes instead of a flat 2s cap
|
||||
- Fixed scheduled tasks refusing their own configured prompt as untrusted input — the fired prompt is now delivered as the session's assigned task
|
||||
- Fixed PowerShell tool commands hanging until timeout when a child process waited on standard input (Windows)
|
||||
- Fixed Python scripts under the PowerShell tool crashing with UnicodeDecodeError when reading non-UTF-8 data from standard input (Windows)
|
||||
- Fixed Python scripts run via the PowerShell tool crashing with UnicodeEncodeError on non-ASCII output, and PowerShell 7 error messages containing raw ANSI escape sequences (Windows)
|
||||
- Fixed the PowerShell tool reporting `where.exe`, `fc.exe`, and `diff.exe` as errors when they return a valid negative answer (Windows)
|
||||
- Fixed `>` and `>>` under the PowerShell tool on Windows PowerShell 5.1 writing UTF-16LE files that other tools couldn't read as UTF-8
|
||||
- Fixed a displaced background daemon deleting its successor's control socket on shutdown, which made the next client kill the healthy replacement daemon
|
||||
- Fixed background sessions parked with `←` or `/background` and left idle keeping the background daemon and a worker process alive indefinitely
|
||||
- Fixed completed background sessions being impossible to remove via `claude rm` or the agent view once the background service had gone idle
|
||||
- Fixed background sessions dispatched from a non-git folder being impossible to delete from the agents view
|
||||
- Fixed reopening a stopped background session failing to restore its saved conversation when an unreadable folder exists in the session store
|
||||
- Fixed the Remote Control "session ready" push notification firing for sessions where Remote Control was not explicitly enabled
|
||||
- Fixed `/install-github-app` and the `/mcp` settings menu being blocked in agent-view sessions — they're now refused only in background sessions with no terminal attached
|
||||
- Fixed plugins enabled via the `--settings` CLI flag not loading (regression since v2.1.181)
|
||||
- Fixed feature flags going stale in long-running sessions after the OAuth token rotates
|
||||
- Fixed `/ultrareview` refusing to run in repos with no merge base — it now offers to review all tracked files
|
||||
- Fixed `claude update` and `claude doctor` hanging silently, and the `/status` System diagnostics section going blank, when a shell-config path is a directory
|
||||
- Fixed memory frontmatter values being silently truncated at an inline `#` when memory files are saved
|
||||
- Fixed session cost and token telemetry double-counting on streams that emit multiple cumulative `message_delta` frames
|
||||
- Fixed a spurious "check your network" warning that appeared while the advisor was thinking
|
||||
- Fixed hooks with exit code 2 not blocking as documented when the hook's stdout JSON fails schema validation
|
||||
- Fixed OTel log events emitted outside the turn's async context missing the interaction span's trace context
|
||||
- Fixed MCP transient errors during prompts/resources refresh clearing the server's slash commands and resources
|
||||
- Improved the `claude rc` workspace-trust error in the home directory to say trust there is never saved and to suggest running from a project directory
|
||||
- Changed single-segment `dir/**` hook `if:` conditions to match only `<cwd>/dir`; write `**/dir/**` for any-depth matching. `deny`/`ask` permission rules keep their any-depth match.
|
||||
- Changed `file` commands using `-m`/`--magic-file` or `-f`/`--files-from` to require permission instead of being auto-allowed as read-only
|
||||
- Changed keep-alive connection pooling to disable after a stale-connection error, so retries open a fresh socket
|
||||
- Changed SessionStart hooks to report source `"fork"` when a session begins as a fork instead of `"resume"`
|
||||
|
||||
## 2.1.212
|
||||
|
||||
- `/fork` now copies your conversation into a new background session (its own row in `claude agents`) while you keep working; the in-session subagent it used to launch is now `/subtask`
|
||||
- Added `claude auto-mode reset` to restore the default auto-mode configuration, with a confirmation prompt (pass `--yes` to skip)
|
||||
- Added a session-wide limit on WebSearch tool calls (default 200, tunable via `CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION`) to stop runaway search loops
|
||||
- Added a per-session cap on subagent spawns (default 200, override with `CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION`) to stop runaway delegation loops; `/clear` resets the budget
|
||||
- MCP tool calls running longer than 2 minutes now move to the background automatically so the session stays usable; configure the threshold or disable with `CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS`
|
||||
- Typing `/resume` in the agent view now opens a picker of past sessions — including sessions deleted from the list — and resumes your pick as a background session
|
||||
- Fixed plan mode auto-running file-modifying Bash commands (e.g. `touch`, `rm`) without a permission prompt or SDK `canUseTool` callback
|
||||
- Fixed worktree creation following a repository-committed symlink at `.claude/worktrees`, which could create files outside the repository
|
||||
- Fixed a `continue:false` hook's halt being dropped when the tool fails or completes mid-stream, and hook infrastructure errors being misreported as user rejections
|
||||
- Fixed SIGTERM during a running Bash tool orphaning the command's process tree in print/SDK mode; the CLI now aborts the turn, kills the tree, and exits 143
|
||||
- Fixed `/background` and `claude --bg` failing with "EUNKNOWN: unknown error, uv_spawn" on Windows when Group Policy blocks PowerShell 5.1; the daemon now prefers PowerShell 7
|
||||
- Fixed shell mode (`!`) not executing commands containing file paths while the path autocomplete popup was open
|
||||
- Fixed auto-mode denial notifications rendering broken characters when a long denial reason was truncated mid-emoji
|
||||
- Fixed Ctrl+J not inserting a newline in the agent view dispatch input on terminals with extended key reporting, and surfaced the newline shortcut in the `?` help overlay
|
||||
- Fixed `/ultrareview` rejecting PR references like `#123`, `PR 123`, and pasted PR URLs; error hints now name the command you actually typed
|
||||
- Fixed `/ultrareview <branch>` not fetching the branch from origin when it exists remotely; it now suggests the closest branch name on typos
|
||||
- Fixed `/ultrareview` skipping the billing confirmation in a new conversation after `/clear`
|
||||
- Fixed `/ultrareview`'s "not a git repository" error on Claude Desktop now suggesting the project's repository folder instead of terminal commands
|
||||
- Fixed hosted (host-managed) sessions failing at startup when repository settings configured mTLS certs, extra CA bundles, or OAuth scopes; these transport settings are now ignored with a warning
|
||||
- Fixed a spurious "File has not been read yet" error when editing a file that had been read with offset/limit before resuming a session
|
||||
- Fixed `ExitWorktree` failing with "no active EnterWorktree session" after resuming a session with `--continue`/`--resume` in print/SDK mode
|
||||
- Fixed the workflow agent grid staying empty for Remote Control clients that join a session mid-run
|
||||
- Fixed streaming-mode control requests being marked complete before their handler finished, which could lose the request on session restart
|
||||
- Fixed background sessions created with `/fork` losing their live-parent protection after a state write failure
|
||||
- Fixed reopening a stopped background session from the agent view failing silently — it now resumes the session, or shows why it can't and lets you force a restart
|
||||
- Fixed agent teams: a stopping teammate could send the leader duplicate idle notifications when team initialization re-ran within a session
|
||||
- Fixed the plan-approval dialog footer splitting "ctrl+g to edit in <editor>" apart when the file path is long
|
||||
- Fixed the welcome banner keeping its old panel widths after a combined width+height terminal resize in fullscreen mode
|
||||
- Fixed diff previews losing their line numbers and +/- markers in narrow layouts
|
||||
- Fixed @-mentions attaching nothing after a partial file read, plugin uninstall targeting the wrong marketplace, and false "Command timed out" on exit code 143
|
||||
- Fixed OpenTelemetry HTTP exports being rejected with 411/400 by Azure Monitor and other endpoints that don't accept chunked transfer encoding
|
||||
- Fixed OTLP event log records missing `trace_id`/`span_id` when `TRACEPARENT` is set in SDK/headless mode
|
||||
- Fixed conversations with many images incorrectly failing with "Request too large" errors, and improved the error message to explain the actual cause
|
||||
- Fixed web search and web fetch returning "API Error" text as search results or page content when the API was overloaded
|
||||
- Improved web search and web fetch reliability by retrying 529 errors and rate-limited requests with bounded backoff
|
||||
- Improved prompt caching: the mid-conversation system block now works behind LLM gateways and custom base URLs (Bedrock, Vertex, 1P)
|
||||
- Improved background agent attach: cold-attaching now instantly shows the formatted transcript while the session boots, instead of a blank wait
|
||||
- Reduced token usage in inter-agent messaging: `SendMessage` bodies are no longer duplicated into replayed history and tool results
|
||||
- Changed `/fork` to name the copy after your prompt when the session has no title, so the row is recognizable in the agent view
|
||||
- Changed bare `/btw` to reopen the side-question panel on your most recent exchange so you can browse earlier answers
|
||||
- Changed the `←` footer hint to pulse `N done` for a moment when a background agent finishes while nothing needs your input
|
||||
- Deprecated the Task tool's `mode` parameter (now ignored); subagents inherit the parent session's permission mode by default
|
||||
- Changed Enterprise `forceLoginMethod` to be enforced for VS Code extension, SDK, `setup-token`, and `install-github-app` logins, not just the terminal
|
||||
- Changed session transcripts to record the reasoning effort level on each assistant message
|
||||
- Changed headless/SDK sessions to apply a `set_model` control request mid-turn; the next model round-trip uses the new model instead of waiting for the next turn
|
||||
- Changed agent view / `claude agents --json`: sessions waiting on a sandbox, MCP-input, or managed-settings prompt now show as "Needs input" instead of "Working"
|
||||
- Updated the auth status panel title from "Cloud authentication" to "Authentication"
|
||||
- Corrected an earlier release note (2.1.200): tmux through the 3.6 series lacks synchronized output; newer tmux with support is detected automatically
|
||||
|
||||
## 2.1.211
|
||||
|
||||
- Added `--forward-subagent-text` flag and `CLAUDE_CODE_FORWARD_SUBAGENT_TEXT` environment variable to include subagent text and thinking in stream-json output
|
||||
- Fixed permission previews relayed to chat channels not neutralizing bidirectional-override, zero-width, and look-alike quote characters, so tool inputs cannot visually alter the approval message
|
||||
- Fixed auto mode overriding a PreToolUse hook's `ask` decision for unsandboxed Bash — a hook `ask` now floors the decision at a prompt
|
||||
- Fixed parallel Claude Code sessions all logging out simultaneously after wake-from-sleep when many sessions share one credential store
|
||||
- Fixed plugin MCP servers not reconnecting after an idle web session woke, leaving MCP calls failing until the next message
|
||||
- Fixed Claude Code on Vertex and Bedrock attempting the default Opus model at startup and printing a spurious fallback notice when a model is explicitly configured
|
||||
- Fixed subagents spawned with an explicit model override reverting to the parent's model when resumed or sent a follow-up message
|
||||
- Fixed nested `.claude/rules/*.md` files loading even when setting sources exclude project settings
|
||||
- Fixed file upload validation: filenames ending in a DOS device suffix (`.prn`) or trailing dot are now accepted, and files with multiple hard links are refused
|
||||
- Fixed file uploads to Claude in Chrome from remote and CLI sessions
|
||||
- Fixed edits that leave the input as "?" being silently swallowed and toggling the shortcuts panel
|
||||
- Fixed a startup hang when the Claude in Chrome extension is enabled but Chrome is not running
|
||||
- Fixed a 300ms delay revealing async content (Settings tabs, Stats, diff views, and other loading states)
|
||||
- Fixed reopening a just-stopped background session from the agents view starting a blank conversation under the same session id
|
||||
- Fixed `/loop` hiding the session from `/resume` after a single use
|
||||
- Fixed screen reader users losing the audible terminal bell after `/terminal-setup` or onboarding terminal setup
|
||||
- Fixed background jobs on LLM gateway auth (`ANTHROPIC_AUTH_TOKEN` + `ANTHROPIC_BASE_URL`) coming back "Not logged in" after the daemon respawns them
|
||||
- Fixed `claude agents` jobs becoming permanently undeletable when git no longer recognizes their worktree — the row now shows why the delete was refused instead of silently reappearing
|
||||
- Fixed `/clear` not resetting the session cost counter — the statusline's cost now starts at $0 after `/clear`
|
||||
- Fixed Claude in Chrome setup pages failing to open in the browser on Windows
|
||||
- Fixed headless print-mode sessions on Windows crashing or silently exiting when stdin is unreadable
|
||||
- Fixed background session titles in the agents view showing the naming model's refusal text when the prompt contains a link
|
||||
- Fixed background agents killed by the user auto-respawning, and revived agents re-running stale prompts from old sessions
|
||||
- Fixed routines with no schedule reporting a next run time in the year 1
|
||||
- Hardened synced skill/plugin directory naming on Windows and kept CCR web fetch/search proxies working after `/clear`
|
||||
- Improved terminal layout and rendering performance
|
||||
- Improved background agent result reporting — Claude now reports the status of still-running agents and waits for the real completion instead of fabricating results
|
||||
- Improved the memory index over-limit warning to measure only loaded content, excluding frontmatter and HTML comments
|
||||
- Updated integer environment variables (timeouts, token budgets, retry counts) to accept scientific notation and digit-separator spellings like `1e6` and `64_000`
|
||||
- Updated documentation links to the current docs sites
|
||||
- Changed "always allow" permission rules to save at the repository root, so approvals granted in a git worktree persist across sessions and worktrees
|
||||
- Changed `/usage-credits` to ask for confirmation before sending a request to organization admins
|
||||
- Changed Vim mode `s` and `S` (substitute char/line) to work in NORMAL mode, matching vim behavior
|
||||
- [VSCode] Updated the Remote Control banner to describe what it does
|
||||
- Claude in Chrome: hardened file-upload path validation
|
||||
- Claude in Chrome: `save_to_disk` on screenshot actions now writes the image to disk and returns the path; previously it did nothing
|
||||
- Fixed a prompt-caching regression on Bedrock, Vertex, Mantle, and Foundry that billed the trailing system context block as fresh input tokens on every request.
|
||||
|
||||
## 2.1.210
|
||||
|
||||
- Added a live elapsed-time counter to the collapsed tool summary line so long-running tool calls visibly tick instead of looking stuck
|
||||
- Added a startup warning for `Write(path)`, `NotebookEdit(path)`, and `Glob(path)` permission rules — use `Edit(path)` or `Read(path)` instead
|
||||
- Fixed `isolation: 'worktree'` subagents being able to run git-mutating commands against the main repo checkout instead of their own isolated worktree
|
||||
- Fixed the `ultracode` keyword opt-in firing on non-human-originated input such as webhook payloads and relayed PR comments
|
||||
- Fixed a rendered text fragment leaking into crash telemetry when a UI component returned content outside a styled text element
|
||||
- Fixed paste markers leaking into external editors opened from Claude Code, which could appear as stray È/É characters around pasted text
|
||||
- Fixed `claude attach` sometimes failing with "job not found" or "agent is still starting" errors during session transitions — attach now waits for the daemon to settle, and terminal resizes during a slow attach are applied once it completes
|
||||
- Fixed a session crash when a tool's result renderer returned a numeric bigint value or plain text instead of a UI element
|
||||
- Fixed a hook callback timeout being misreported to the model as a user rejection, which made unattended sessions stop and wait
|
||||
- Fixed Claude assuming a `cd` took effect after its command was moved to the background; the tool result now states the working directory is unchanged
|
||||
- Fixed plugin-provided MCP servers being torn down when MCP servers are re-synced mid-session
|
||||
- Fixed plan approvals without edits being labeled "(edited by user)" and overwriting the plan file with a stale snapshot
|
||||
- Fixed `/doctor` skipping its auto-mode-default proposal on Bedrock, Vertex, and Foundry, where auto mode no longer needs an opt-in
|
||||
- Fixed Grep content mode claiming "No matches found" when paginating past the end of results
|
||||
- Fixed unmatched `$1`/`$2` positional placeholders in skills and commands being silently stripped; they are now preserved verbatim
|
||||
- Fixed plugin cache writes leaving temp files behind on failure and failing on locked-file renames on Windows and network filesystems
|
||||
- Fixed background workers crash-looping when a client resets its connection to the background service
|
||||
- Fixed `claude agents --effort ultracode` not reaching dispatched sessions; the value was silently dropped
|
||||
- Fixed pressing ← to open the agents view dropping the task tracker when returning to the session
|
||||
- Fixed the agents dashboard retaining pasted images from abandoned reply drafts after their session was deleted
|
||||
- Fixed killed background sessions leaving a permanent `git worktree lock` behind; the periodic sweep now releases locks whose owning process is gone
|
||||
- Fixed SDK MCP servers registered via an `initialize` control request waiting until the next turn to start connecting
|
||||
- Fixed returning to the agents view from a session leaving overlapping ghost frames with `CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1`
|
||||
- Fixed late-appearing `.claude/*` symlinks not being reconciled into the sandbox deny-write list
|
||||
- Hardened the Agent tool against indirect prompt injection via content a subagent read
|
||||
- Improved the Bash/PowerShell tool message when a command hits its timeout and is auto-backgrounded, so the model can distinguish a hang from an explicit background request
|
||||
- Improved auto mode: the permission classifier now defaults to Sonnet 5 for external sessions, validated on the session's first request and pinned for the session
|
||||
- Improved the bundled dataviz skill's chart color validation with perceptual OKLab color difference and recalibrated color-blindness thresholds
|
||||
- Memory writes that leave a MEMORY.md index over its read limit now produce an explicit error instead of silent truncation
|
||||
- Screen reader mode now announces permission mode changes aloud when cycling modes with Shift+Tab
|
||||
- The agents footer hint now shows how many background agents are waiting on your input, with a brief color emphasis when the count changes
|
||||
- Agent view: the session you pressed ← from stays visibly marked even after mouse hover or arrow keys move the selection
|
||||
- Fable temporarily shows as unavailable in the advisor picker while a server-side issue causing Fable advisor failures is fixed
|
||||
|
||||
## 2.1.209
|
||||
|
||||
- Fixed /model and other dialogs being blocked in `claude agents` background sessions (reverts an overly broad guard)
|
||||
@@ -10,6 +257,7 @@
|
||||
- Added `vimInsertModeRemaps` setting: map two-key insert-mode sequences like `jj` to Escape in vim mode
|
||||
- Added `CLAUDE_CODE_PROCESS_WRAPPER`: agent view and the background service now honor a corporate launcher by running every Claude Code self-spawn through a required wrapper executable
|
||||
- Added mouse-click support for multi-select menus and "Other" input rows in fullscreen mode
|
||||
- Changed the Fable 5 usage-credits consent prompt to start with the decline option focused
|
||||
- Fixed fast mode staying off after switching back to a model that supports it — it now restores automatically when enabled in settings
|
||||
- Fixed replies typed to a background agent being lost when delivery fails — the text is now saved and delivered when the session restarts
|
||||
- Fixed background-session attach failing permanently ("Couldn't start the background daemon") after an update replaced the binary a running `claude agents` process was launched from
|
||||
@@ -3483,7 +3731,7 @@
|
||||
|
||||
## 2.1.15
|
||||
|
||||
- Added deprecation notification for npm installations - run `claude install` or see https://docs.anthropic.com/en/docs/claude-code/getting-started for more options
|
||||
- Added deprecation notification for npm installations - run `claude install` or see https://code.claude.com/docs/en/setup for more options
|
||||
- Improved UI rendering performance with React Compiler
|
||||
- Fixed the "Context left until auto-compact" warning not disappearing after running `/compact`
|
||||
- Fixed MCP stdio server timeout not killing child process, which could cause UI freezes
|
||||
|
||||
19
examples/gateway/aws/.dockerignore
Normal file
19
examples/gateway/aws/.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
||||
# Keep secrets and generated artifacts out of the build context. The Dockerfile
|
||||
# COPYs the binary, gateway.yaml (unlike the GCP example, the config is baked
|
||||
# into the image — ECS injects only the secrets it references, as env vars),
|
||||
# and the RDS CA bundle. BuildKit (the default builder) only syncs the
|
||||
# referenced COPY sources anyway, so this is a denylist for the classic
|
||||
# builder (DOCKER_BUILDKIT=0) and a conventional signal that the .gitignore'd
|
||||
# secrets in this directory aren't part of the image build.
|
||||
terraform/
|
||||
**/.terraform/
|
||||
*.tfstate*
|
||||
terraform.tfvars
|
||||
secrets/
|
||||
*.pem
|
||||
# The RDS CA bundle is public trust-anchor material (no secret), and the
|
||||
# Dockerfile COPYs it — carve it out of the *.pem exclusion above.
|
||||
!rds-global-bundle.pem
|
||||
*.iam.json
|
||||
claude.download
|
||||
claude.bad
|
||||
18
examples/gateway/aws/.gitignore
vendored
Normal file
18
examples/gateway/aws/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Local, environment-specific config — copy gateway.yaml.example -> gateway.yaml
|
||||
# (gateway.yaml.example IS committed; your filled-in gateway.yaml is not)
|
||||
gateway.yaml
|
||||
|
||||
# Secrets / credentials — never commit. Also covers rds-global-bundle.pem:
|
||||
# not a secret, but downloaded by setup.sh when absent (delete it to refresh
|
||||
# after an RDS CA rotation), so it stays out of git.
|
||||
secrets/
|
||||
*.pem
|
||||
|
||||
# Scratch IAM policy documents written by setup.sh (no secrets, but generated)
|
||||
*.iam.json
|
||||
|
||||
# Vendored release binary — download per release (see setup.sh DIST_URL).
|
||||
# claude.bad is a checksum-mismatched binary that setup.sh set aside.
|
||||
claude
|
||||
claude.download
|
||||
claude.bad
|
||||
67
examples/gateway/aws/Dockerfile
Normal file
67
examples/gateway/aws/Dockerfile
Normal file
@@ -0,0 +1,67 @@
|
||||
# Runtime image for `claude gateway`.
|
||||
#
|
||||
# This image does NOT build the binary. It expects a prebuilt native
|
||||
# linux-x64 `claude` executable in the build context — the Claude Code release
|
||||
# binary, which includes the `gateway` subcommand. setup.sh places it at
|
||||
# ./claude (downloading and checksum-verifying it via DIST_URL/DIST_SHA256 if
|
||||
# missing). Override CLAUDE_BINARY to point at a different path.
|
||||
#
|
||||
# Unlike the GCP example (which mounts the config from Secret Manager at
|
||||
# runtime), this image BAKES gateway.yaml in at /etc/claude/gateway.yaml — on
|
||||
# ECS the task definition injects only the secrets the YAML references, as env
|
||||
# vars. gateway.yaml therefore must be fully filled in (no REPLACE_ME) before
|
||||
# building; setup.sh enforces this. A config edit means a rebuild under a new
|
||||
# tag. The file contains no secret values — every credential resolves at boot
|
||||
# via ${ENV_VAR} expansion.
|
||||
#
|
||||
# The image also bakes in the AWS RDS CA bundle (rds-global-bundle.pem —
|
||||
# setup.sh downloads it from https://truststore.pki.rds.amazonaws.com before
|
||||
# the build) and trusts it via NODE_EXTRA_CA_CERTS, so the store connection
|
||||
# string's `?sslmode=verify-full` verifies the RDS server certificate chain
|
||||
# and hostname. NOTE the gateway's driver reads `sslmode` from the URL but NOT
|
||||
# a libpq-style `sslrootcert=` param — the CA must come from this env var.
|
||||
#
|
||||
# Build:
|
||||
# docker build --platform=linux/amd64 --provenance=false \
|
||||
# --build-arg CLAUDE_BINARY=./claude -t claude-gateway .
|
||||
#
|
||||
# (For Fargate on ARM64/Graviton: build --platform=linux/arm64 with the
|
||||
# linux-arm64 binary and set the task definition's cpuArchitecture to ARM64.)
|
||||
#
|
||||
# Run:
|
||||
# docker run --rm -p 8080:8080 \
|
||||
# -e OIDC_CLIENT_SECRET -e GATEWAY_JWT_SECRET -e GATEWAY_POSTGRES_URL \
|
||||
# claude-gateway
|
||||
|
||||
ARG CLAUDE_BINARY=./claude
|
||||
ARG GATEWAY_CONFIG=./gateway.yaml
|
||||
ARG RDS_CA_BUNDLE=./rds-global-bundle.pem
|
||||
|
||||
# distroless/cc provides glibc + libstdc++ (required by the Bun-compiled
|
||||
# native binary). The :nonroot tag runs as uid/gid 65532. Pinned by digest so
|
||||
# the build never silently takes new upstream bytes (the digest is the
|
||||
# multi-arch OCI index, so --platform still selects amd64/arm64). To refresh
|
||||
# the pin after reviewing upstream changes:
|
||||
# docker manifest inspect -v gcr.io/distroless/cc-debian12:nonroot # prints the index digest
|
||||
FROM gcr.io/distroless/cc-debian12:nonroot@sha256:ce0d66bc0f64aae46e6a03add867b07f42cc7b8799c949c2e898057b7f75a151
|
||||
|
||||
ARG CLAUDE_BINARY
|
||||
ARG GATEWAY_CONFIG
|
||||
ARG RDS_CA_BUNDLE
|
||||
COPY --chmod=0755 ${CLAUDE_BINARY} /usr/local/bin/claude
|
||||
# WORKDIR pre-creates /etc/claude with 0755 — without it, COPY --chmod would
|
||||
# also stamp the auto-created parent directory 0644 (no execute bit), making
|
||||
# the config unreadable for the nonroot user.
|
||||
WORKDIR /etc/claude
|
||||
COPY --chmod=0644 ${GATEWAY_CONFIG} /etc/claude/gateway.yaml
|
||||
COPY --chmod=0644 ${RDS_CA_BUNDLE} /etc/claude/rds-global-bundle.pem
|
||||
WORKDIR /
|
||||
|
||||
ENV CLAUDE_CONFIG_DIR=/tmp/.claude
|
||||
# Trust anchor for the store's sslmode=verify-full (see header comment).
|
||||
ENV NODE_EXTRA_CA_CERTS=/etc/claude/rds-global-bundle.pem
|
||||
|
||||
EXPOSE 8080
|
||||
USER nonroot
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/claude", "gateway", "--config", "/etc/claude/gateway.yaml"]
|
||||
19
examples/gateway/aws/README.md
Normal file
19
examples/gateway/aws/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Claude apps gateway on AWS
|
||||
|
||||
Reference deployment artifacts for running Claude apps gateway on AWS with
|
||||
Amazon Bedrock as the upstream: ECS on Fargate or EKS, Amazon RDS for
|
||||
PostgreSQL, AWS Secrets Manager, and IAM-role auth to Bedrock.
|
||||
|
||||
These files are provided as a working example rather than a supported production
|
||||
deployment. Adapt them to your own environment.
|
||||
|
||||
- **Walkthrough**: https://code.claude.com/docs/en/claude-apps-gateway-on-aws
|
||||
- **Related**: AWS-maintained samples for various customer environments at
|
||||
https://github.com/aws-samples/anthropic-on-aws/tree/main/claude-apps-gateway
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `setup.sh` | Scripts the walkthrough end to end via the `aws` CLI |
|
||||
| `Dockerfile` | Runtime image for the `claude gateway` binary (bakes in `gateway.yaml`) |
|
||||
| `gateway.yaml.example` | Gateway config template, AWS-shaped (Bedrock upstream, Okta IdP) |
|
||||
| `terraform/` | Provisions the full architecture (two-pass apply — see `terraform/README.md`) |
|
||||
174
examples/gateway/aws/gateway.yaml.example
Normal file
174
examples/gateway/aws/gateway.yaml.example
Normal file
@@ -0,0 +1,174 @@
|
||||
# gateway.yaml.example — Claude apps gateway config template, AWS-shaped (walkthrough §4).
|
||||
#
|
||||
# Okta IdP + Bedrock upstream, following the walkthrough at
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-on-aws. The active sections
|
||||
# below are a strict subset of the full configuration reference at
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-config; optional keys are
|
||||
# included commented-out.
|
||||
#
|
||||
# USAGE — this is the shippable TEMPLATE. Copy it to gateway.yaml and fill it in:
|
||||
# cp gateway.yaml.example gateway.yaml
|
||||
# setup.sh and terraform/ read gateway.yaml (your filled-in copy, which is
|
||||
# gitignored). Unlike the GCP example it is NOT published to a secret store:
|
||||
# the Dockerfile bakes it into the image at /etc/claude/gateway.yaml — the
|
||||
# container ENTRYPOINT runs `claude gateway --config /etc/claude/gateway.yaml`.
|
||||
# It holds no secret values; a config edit means an image rebuild (setup.sh
|
||||
# tags images with a hash of this file, so a re-run rebuilds automatically).
|
||||
#
|
||||
# Secret expansion: ${ENV_VAR} reads an env var; ${file:/path} reads a mounted file.
|
||||
# On ECS, the task definition injects the JWT / OIDC / Postgres secrets as ENV
|
||||
# VARS via its `secrets` field (valueFrom -> Secrets Manager ARN). On EKS you
|
||||
# may mount them as files instead and use ${file:/secrets/...}.
|
||||
#
|
||||
# BEFORE BUILD — replace every REPLACE_ME placeholder below (setup.sh refuses to
|
||||
# build the image while any remain — the config is baked in, so a half-filled
|
||||
# config would ship), and create the referenced secrets:
|
||||
# gateway-jwt-secret (setup.sh generates this)
|
||||
# gateway-oidc-client-secret (from the Okta admin console OIDC web app)
|
||||
# gateway-postgres-url (setup.sh generates this)
|
||||
|
||||
# ── Listener ─────────────────────────────────────────────────────────────────
|
||||
listen:
|
||||
host: 0.0.0.0
|
||||
port: 8080 # the target group forwards ALB :443 -> :8080
|
||||
# Required. Fixes the IdP redirect_uri, the OIDC discovery doc, and the
|
||||
# gateway-token issuer so none are derived from the client-controlled Host
|
||||
# header (X-Forwarded-Host/-Proto are likewise never trusted). Set it to the
|
||||
# internal hostname you picked in the prerequisites — the Route 53 private
|
||||
# zone name your ACM certificate covers (e.g.
|
||||
# https://claude-gateway.internal.example.com). Unlike Cloud Run there is no
|
||||
# first-deploy placeholder dance: you choose the hostname up front, alias it
|
||||
# to the internal ALB after the deploy, and register the same host's
|
||||
# /oauth/callback on the Okta app.
|
||||
public_url: REPLACE_ME
|
||||
# Register this exact redirect URI on the Okta OIDC web application:
|
||||
# https://<public_url host>/oauth/callback
|
||||
#
|
||||
# Behind the internal ALB every request arrives via the load balancer, so the
|
||||
# gateway sees ALB-node peer IPs for all developers — set trusted_proxies so
|
||||
# X-Forwarded-For from those proxies is trusted and per-IP rate limiting /
|
||||
# audit IPs record the real client. ALB nodes take addresses from the subnets
|
||||
# the ALB is attached to, so list those subnets' CIDRs (the private subnets
|
||||
# from the prerequisites).
|
||||
#
|
||||
# NOTE: listing the ALB subnets' CIDRs trusts every host in those subnets as a
|
||||
# proxy — any co-located workload that can reach the ALB can then spoof the
|
||||
# client IP via X-Forwarded-For (audit logs, per-IP rate limits, IP
|
||||
# allowlists). Keep the ALB :443 ingress source (CORP_CIDR / corporate_cidr)
|
||||
# from overlapping these subnets, and don't share the subnets with untrusted
|
||||
# workloads.
|
||||
trusted_proxies: [REPLACE_ME] # e.g. [10.0.1.0/24, 10.0.2.0/24]
|
||||
#
|
||||
# Alternative — terminate TLS in the gateway itself instead of at the ALB:
|
||||
# tls:
|
||||
# cert: /certs/gateway.crt
|
||||
# key: /certs/gateway.key
|
||||
|
||||
# ── Identity provider — Okta ─────────────────────────────────────────────────
|
||||
oidc:
|
||||
issuer: REPLACE_ME # e.g. https://example.okta.com (or your custom auth server URL)
|
||||
client_id: REPLACE_ME # Okta OIDC web app client ID (not secret)
|
||||
client_secret: ${OIDC_CLIENT_SECRET} # EKS file mounts: ${file:/secrets/oidc-client-secret}
|
||||
allowed_email_domains: [REPLACE_ME] # e.g. [example.com] — reject id_tokens outside your org
|
||||
# The Okta org authorization server returns a thin id_token that omits email
|
||||
# and groups; the gateway fills them from /userinfo.
|
||||
userinfo_fallback: true
|
||||
# offline_access yields refresh tokens (silent renewal + the deprovision
|
||||
# leash); Okta emits groups only when the `groups` scope is requested AND the
|
||||
# app's groups claim filter allows them (Okta admin console -> the app's
|
||||
# Sign On tab -> OpenID Connect ID Token -> Groups claim filter).
|
||||
scopes: [openid, profile, email, offline_access, groups]
|
||||
# groups_claim: groups # Okta default. Entra app roles=roles; see the config reference
|
||||
# ca_cert_pem: ${file:/secrets/idp-ca.pem} # only for an IdP behind a private CA
|
||||
|
||||
# ── Sessions ─────────────────────────────────────────────────────────────────
|
||||
session:
|
||||
jwt_secret: ${GATEWAY_JWT_SECRET} # >= 32 bytes; openssl rand -base64 32
|
||||
# Okta issues refresh tokens (offline_access above), so sessions renew
|
||||
# silently and this mainly bounds deprovision latency. 8 is a sane default;
|
||||
# lower toward 1 for tighter revocation. Array form rotates keys:
|
||||
# [new, old] (index 0 signs, all verify).
|
||||
ttl_hours: 8
|
||||
|
||||
# ── Store (REQUIRED — the gateway refuses to boot without it) ─────────────────
|
||||
store:
|
||||
postgres_url: ${GATEWAY_POSTGRES_URL} # private-subnet RDS; built with ?sslmode=verify-full by setup.sh
|
||||
# (the image trusts the RDS CA bundle via NODE_EXTRA_CA_CERTS — see Dockerfile)
|
||||
|
||||
# ── Upstreams — Amazon Bedrock ───────────────────────────────────────────────
|
||||
upstreams:
|
||||
- provider: bedrock
|
||||
# Must equal the region you provision in (setup.sh's AWS_REGION /
|
||||
# terraform's region): the IAM policy's inference-profile ARNs are scoped
|
||||
# to that region, and Bedrock model access is enabled there (cross-region
|
||||
# us.anthropic.* profiles need access in every spanned region). NOTE: the
|
||||
# walkthrough is scoped to US regions — the built-in model catalog maps to
|
||||
# us.anthropic.* (US-geo) profiles; a non-US region also needs a models:
|
||||
# list below (see the model catalog section).
|
||||
region: REPLACE_ME # e.g. us-east-1
|
||||
auth: {} # AWS default credential chain: ECS task role / IRSA on EKS (preferred — no static keys)
|
||||
# base_url: https://bedrock-runtime.us-east-1.amazonaws.com # bedrock-runtime interface VPC endpoint, to keep model traffic off the public path
|
||||
# Add more upstreams for failover (tried top→bottom on 5xx/timeout/501): a
|
||||
# second region, or an anthropic/vertex fallback. See
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway.
|
||||
|
||||
# ── Telemetry fan-out (OPTIONAL) ─────────────────────────────────────────────
|
||||
# The CLI sends OTLP/HTTP to the gateway; the gateway fans out, stamping
|
||||
# user.id/user.email/user.groups server-side. On AWS, point at an OpenTelemetry
|
||||
# Collector (e.g. the AWS Distro for OpenTelemetry -> CloudWatch / Managed
|
||||
# Prometheus). When forward_to and public_url are both configured the gateway
|
||||
# pushes CLAUDE_CODE_ENABLE_TELEMETRY and the OTEL exporter selectors to every
|
||||
# client automatically — no per-developer config needed.
|
||||
# telemetry:
|
||||
# forward_to:
|
||||
# - url: https://otel-collector.internal.example.com:4318
|
||||
# headers:
|
||||
# Authorization: ${file:/secrets/otlp-token}
|
||||
# metrics: true # safe aggregate counters (default)
|
||||
# logs: false # carries bash commands / tool inputs — opt in deliberately
|
||||
# traces: false
|
||||
|
||||
# ── RBAC + managed settings (OPTIONAL; first-match-wins, top -> bottom) ───────
|
||||
# With Okta as IdP, match on the group names the `groups` scope emits (subject
|
||||
# to the app's groups claim filter), or on email_domain.
|
||||
# managed:
|
||||
# policies:
|
||||
# - match: { groups: [engineering] }
|
||||
# cli:
|
||||
# availableModels: [claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5]
|
||||
# permissions: { deny: ["Read(./.env)", "Read(./secrets/**)"] }
|
||||
# - match: {} # catch-all floor — keep LAST
|
||||
# cli:
|
||||
# availableModels: [claude-sonnet-4-6, claude-haiku-4-5]
|
||||
|
||||
# ── Admin API (OPTIONAL — enables db-mode runtime config + spend caps) ───────
|
||||
# admin_groups needs a groups claim — Okta provides one via the `groups` scope
|
||||
# above — or use the bootstrap keys below instead. Named keys for attribution
|
||||
# in the audit log; 32-char minimum on key values. On ECS add these to the task
|
||||
# definition's `secrets` field (valueFrom -> a Secrets Manager ARN), same as the
|
||||
# JWT/OIDC/Postgres secrets above; on EKS you may use ${file:...}.
|
||||
# admin:
|
||||
# write_keys:
|
||||
# - id: terraform
|
||||
# key: ${GATEWAY_ADMIN_WRITE_KEY}
|
||||
# read_keys:
|
||||
# - id: reporting
|
||||
# key: ${GATEWAY_ADMIN_READ_KEY}
|
||||
# # admin_groups: [platform-finops] # Okta group names via the groups scope
|
||||
|
||||
# ── Model catalog (OPTIONAL for US regions) ──────────────────────────────────
|
||||
# Default true: every built-in Claude model is exposed and auto-translated per
|
||||
# upstream (the built-in table already maps to us.anthropic.* cross-region
|
||||
# inference profiles). Set false + a models: list to pin IDs (e.g. an
|
||||
# application or provisioned-throughput inference-profile ARN).
|
||||
# NON-US REGIONS: the built-in us.anthropic.* mappings do not exist outside
|
||||
# the US geo — set auto_include_builtin_models: false and list your region's
|
||||
# inference profiles (eu.anthropic.*, apac.anthropic.*, ...) here, and widen
|
||||
# the geo prefix in the deploy's bedrock-invoke IAM policy to match. See the
|
||||
# models: guidance in the config reference:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-config
|
||||
# auto_include_builtin_models: true
|
||||
# models:
|
||||
# - id: claude-opus-4-8
|
||||
# label: Claude Opus 4.8
|
||||
# upstream_model: { bedrock: us.anthropic.claude-opus-4-8 }
|
||||
964
examples/gateway/aws/setup.sh
Executable file
964
examples/gateway/aws/setup.sh
Executable file
@@ -0,0 +1,964 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# setup.sh — AWS setup for Claude apps gateway (walkthrough §1–7, ECS track).
|
||||
#
|
||||
# Provisions, in this order: the three security groups (§1), the task +
|
||||
# execution IAM roles (§2), the gateway container image in Amazon ECR (§6),
|
||||
# an RDS for PostgreSQL instance in the private subnets with no public
|
||||
# address (§3), the JWT + postgres-url secrets (§5), and an ECS Fargate
|
||||
# service behind an internal Application Load Balancer (§7).
|
||||
#
|
||||
# gateway.yaml (§4 of the walkthrough) is BAKED INTO THE IMAGE on this track —
|
||||
# the task definition injects only the secrets it references, as env vars — so
|
||||
# the config step here lives inside the image build (§6): the build is gated on
|
||||
# a fully filled-in gateway.yaml and the image tag carries a hash of it, so a
|
||||
# config edit triggers a rebuild on the next run.
|
||||
#
|
||||
# Section markers (§N) below map to the walkthrough:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-on-aws
|
||||
#
|
||||
# Covers here: security groups (§1) -> IAM roles + Bedrock model-access note (§2)
|
||||
# -> build & push image, config baked in (§6 + §4) -> DB subnet group
|
||||
# + RDS instance (§3) -> jwt + postgres-url secrets (§5) -> ECS
|
||||
# cluster/task definition/service + internal ALB (§7, ECS Fargate tab).
|
||||
# Not covered: EKS track (§7's EKS tab) — ECS Fargate is the lower-friction path here.
|
||||
# Bedrock model access (§2) — console-only; the script reminds you.
|
||||
# Route 53 alias — see the next steps it prints. Client MDM
|
||||
# push (§8) is covered by the walkthrough, not this script.
|
||||
#
|
||||
# Idempotent: existing resources are detected and skipped, so it is safe to re-run.
|
||||
# Reuse is by NAME, so a pre-existing resource may not match what this script
|
||||
# would have created: reuse that would change the exposure model is fatal (an
|
||||
# ALB that is not internal/in ${VPC_ID}); upsert-able settings are converged on
|
||||
# every run; other posture drift (extra security group ingress, a public or
|
||||
# unencrypted RDS instance, wrong-VPC target group) is checked and warned
|
||||
# about, never silently adopted.
|
||||
# Override any default below via environment variable, e.g. `AWS_REGION=us-west-2 ./setup.sh`.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- configuration (env-overridable) ----------------------------------------
|
||||
AWS_REGION="${AWS_REGION:-$(aws configure get region 2>/dev/null || true)}" # guide uses us-east-1 (a region where Bedrock serves the Claude models you need)
|
||||
ACCOUNT_ID="${ACCOUNT_ID:-$(aws sts get-caller-identity --query Account --output text 2>/dev/null || true)}"
|
||||
|
||||
VPC_ID="${VPC_ID:-}" # REQUIRED — the VPC from the prerequisites
|
||||
PRIVATE_SUBNETS="${PRIVATE_SUBNETS:-}" # REQUIRED — two+ private subnet IDs in different AZs, space-separated
|
||||
CORP_CIDR="${CORP_CIDR:-}" # REQUIRED — your corporate network CIDR (ALB :443 ingress source)
|
||||
# Must not overlap PRIVATE_SUBNETS: hosts there are trusted_proxies (gateway.yaml) and could spoof client IPs via X-Forwarded-For.
|
||||
|
||||
# §1 security groups
|
||||
ALB_SG_NAME="${ALB_SG_NAME:-claude-gateway-alb}"
|
||||
GW_SG_NAME="${GW_SG_NAME:-claude-gateway-svc}"
|
||||
DB_SG_NAME="${DB_SG_NAME:-claude-gateway-db}"
|
||||
|
||||
# §2 IAM roles (task role = the gateway's runtime AWS identity; execution role
|
||||
# = the ECS agent's identity for pulling the image and injecting secrets)
|
||||
TASK_ROLE="${TASK_ROLE:-claude-gateway-task}"
|
||||
EXEC_ROLE="${EXEC_ROLE:-claude-gateway-execution}"
|
||||
|
||||
# §6 image
|
||||
ECR_REPO="${ECR_REPO:-claude-gateway}" # ECR repository name
|
||||
VERSION="${VERSION:-}" # REQUIRED — the gateway release tag you build and push (e.g. the linux-x64 binary's version)
|
||||
DOCKERFILE="${DOCKERFILE:-./Dockerfile}"
|
||||
CLAUDE_BINARY="${CLAUDE_BINARY:-./claude}" # prebuilt linux-x64 Claude Code release binary (includes the gateway subcommand)
|
||||
DIST_URL="${DIST_URL:-}" # optional: download URL, used only if $CLAUDE_BINARY is missing
|
||||
DIST_SHA256="${DIST_SHA256:-}" # REQUIRED with DIST_URL: expected sha256 of the binary (verified fail-closed)
|
||||
DIST_SHA256="${DIST_SHA256,,}" # normalize to lowercase — openssl emits lowercase hex; some tools (PowerShell Get-FileHash) publish uppercase
|
||||
# Obtain DIST_SHA256 out-of-band — never from the server that serves DIST_URL.
|
||||
# For binaries from the standard Claude Code release channel, verify the
|
||||
# release's GPG-signed manifest.json and copy the platform checksum from it:
|
||||
# https://code.claude.com/docs/en/setup#binary-integrity-and-code-signing
|
||||
# For any other distribution channel, use the checksum published alongside the
|
||||
# download link on that channel.
|
||||
GATEWAY_YAML="${GATEWAY_YAML:-./gateway.yaml}" # §4 config file — BAKED into the image
|
||||
RDS_CA_BUNDLE="${RDS_CA_BUNDLE:-./rds-global-bundle.pem}" # RDS CA trust anchor — BAKED into the image (downloaded below if missing)
|
||||
# Official AWS RDS truststore. AWS rotates this bundle (new regional CAs get
|
||||
# appended), so no checksum is pinned — a pinned hash would break on every
|
||||
# rotation. The script downloads it only when absent (an existing file is never
|
||||
# re-downloaded); to pick up a rotation, delete the file — and since the image
|
||||
# tag hashes only gateway.yaml, also bump VERSION or set IMAGE_TAG so the
|
||||
# next run rebuilds rather than reusing the existing tag. Operators who want
|
||||
# to pin may pre-place a reviewed copy at ${RDS_CA_BUNDLE}.
|
||||
RDS_CA_BUNDLE_URL="${RDS_CA_BUNDLE_URL:-https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem}"
|
||||
REGISTRY="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
|
||||
# §3 RDS
|
||||
DB_SUBNET_GROUP="${DB_SUBNET_GROUP:-claude-gateway-db}"
|
||||
DB_PARAM_GROUP="${DB_PARAM_GROUP:-claude-gateway-db}" # carries rds.force_ssl=1 (server-side TLS enforcement)
|
||||
DB_INSTANCE="${DB_INSTANCE:-claude-gateway-db}"
|
||||
DB_CLASS="${DB_CLASS:-db.t4g.micro}"
|
||||
DB_STORAGE_GB="${DB_STORAGE_GB:-20}"
|
||||
DB_NAME="${DB_NAME:-claude_gateway}"
|
||||
DB_USER="${DB_USER:-gateway}"
|
||||
# PG14+ supported; 16 is the recommended default (matches terraform/'s).
|
||||
# Always pinned: the instance's engine version and the parameter group's
|
||||
# family must name the same major, so both derive from this one value.
|
||||
DB_ENGINE_VERSION="${DB_ENGINE_VERSION:-16}"
|
||||
|
||||
SECRET_NAME="${SECRET_NAME:-gateway-postgres-url}" # §5 store.postgres_url
|
||||
JWT_SECRET_NAME="${JWT_SECRET_NAME:-gateway-jwt-secret}" # §5 session.jwt_secret
|
||||
OIDC_SECRET_NAME="${OIDC_SECRET_NAME:-gateway-oidc-client-secret}" # operator-created (Okta OIDC web app)
|
||||
# NOTE: the execution role's secrets-read policy (§2) is built from these
|
||||
# three names, one per-secret ARN prefix each — a rename is picked up on the
|
||||
# next run (put-role-policy is an upsert).
|
||||
|
||||
# §7 ECS + internal ALB deploy
|
||||
CLUSTER="${CLUSTER:-claude-gateway}"
|
||||
SERVICE="${SERVICE:-claude-gateway}"
|
||||
TASK_FAMILY="${TASK_FAMILY:-claude-gateway}"
|
||||
LOG_GROUP="${LOG_GROUP:-/ecs/claude-gateway}"
|
||||
LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-90}" # CloudWatch retention — the group carries the gateway's audit events, so align with your audit retention policy
|
||||
ALB_NAME="${ALB_NAME:-claude-gateway}"
|
||||
TG_NAME="${TG_NAME:-claude-gateway}"
|
||||
# Explicit modern TLS policy — omitting it falls back to the legacy
|
||||
# ELBSecurityPolicy-2016-08 default, which still accepts TLS 1.0/1.1.
|
||||
ALB_SSL_POLICY="${ALB_SSL_POLICY:-ELBSecurityPolicy-TLS13-1-2-2021-06}"
|
||||
ACM_CERT_ARN="${ACM_CERT_ARN:-}" # REQUIRED for deploy — ACM cert for your internal gateway hostname
|
||||
TASK_CPU="${TASK_CPU:-1024}"
|
||||
TASK_MEMORY="${TASK_MEMORY:-2048}"
|
||||
DESIRED_COUNT="${DESIRED_COUNT:-1}" # each task opens a Postgres pool of up to 5 connections (store.max_connections default); keep DESIRED_COUNT × 5 below the DB class's max_connections (~80 on db.t4g.micro)
|
||||
DEPLOY="${DEPLOY:-1}" # set DEPLOY=0 to provision only, no ECS/ALB deploy
|
||||
|
||||
# ---- helpers ----------------------------------------------------------------
|
||||
log() { printf '\n==> %s\n' "$*"; }
|
||||
skip() { printf ' (exists) %s\n' "$*"; }
|
||||
curl_https() { curl --proto '=https' --proto-redir '=https' --tlsv1.2 "$@"; } # refuse plaintext/protocol-downgrade
|
||||
sha_of() { openssl dgst -sha256 "$1" | awk '{print $NF}'; } # openssl avoids shasum/sha256sum portability gaps
|
||||
|
||||
# authorize-security-group-ingress is NOT idempotent (re-adding a rule errors),
|
||||
# so tolerate exactly the duplicate-rule error and fail on anything else.
|
||||
authorize_ingress() {
|
||||
local out
|
||||
if out="$(aws ec2 authorize-security-group-ingress "$@" 2>&1)"; then
|
||||
return 0
|
||||
elif grep -q 'InvalidPermission.Duplicate' <<<"${out}"; then
|
||||
skip "ingress rule already present"
|
||||
else
|
||||
printf '%s\n' "${out}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Security-group lookup by name within the VPC; prints the GroupId or "None".
|
||||
sg_id() {
|
||||
aws ec2 describe-security-groups \
|
||||
--filters "Name=group-name,Values=$1" "Name=vpc-id,Values=${VPC_ID}" \
|
||||
--query 'SecurityGroups[0].GroupId' --output text 2>/dev/null || echo None
|
||||
}
|
||||
|
||||
# Name-based reuse can adopt a pre-existing group carrying ingress this script
|
||||
# never added. Audit after the intended rule is ensured: each group's traffic
|
||||
# path is exactly one rule (tcp <port> from <cidr-or-source-group>), so anything
|
||||
# else is flagged on stderr. Non-fatal — an extra rule may be a deliberate
|
||||
# operator addition — but every one widens the path, so it must be visible.
|
||||
warn_unexpected_ingress() { # <group-id> <group-name> <port> <expected cidr or source group-id>
|
||||
local perms
|
||||
if ! perms="$(aws ec2 describe-security-groups --group-ids "$1" \
|
||||
--query 'SecurityGroups[0].IpPermissions' --output json 2>/dev/null)"; then
|
||||
echo " WARN — could not audit ingress rules on $2 ($1)." >&2
|
||||
return 0
|
||||
fi
|
||||
# `|| echo` keeps a parse hiccup non-fatal — this audit must never abort a run.
|
||||
_SG_ID="$1" _SG_NAME="$2" _SG_PORT="$3" _SG_EXPECTED="$4" python3 -c "
|
||||
import json, os, sys
|
||||
perms = json.load(sys.stdin) or []
|
||||
port, expected = int(os.environ[\"_SG_PORT\"]), os.environ[\"_SG_EXPECTED\"]
|
||||
extras = []
|
||||
for p in perms:
|
||||
proto, lo, hi = p.get(\"IpProtocol\"), p.get(\"FromPort\"), p.get(\"ToPort\")
|
||||
scope_ok = proto == \"tcp\" and lo == port and hi == port
|
||||
sources = (
|
||||
[r.get(\"CidrIp\", \"?\") for r in p.get(\"IpRanges\", [])]
|
||||
+ [r.get(\"CidrIpv6\", \"?\") for r in p.get(\"Ipv6Ranges\", [])]
|
||||
+ [r.get(\"GroupId\", \"?\") for r in p.get(\"UserIdGroupPairs\", [])]
|
||||
+ [r.get(\"PrefixListId\", \"?\") for r in p.get(\"PrefixListIds\", [])]
|
||||
)
|
||||
extras += [(proto, lo, hi, s) for s in sources if not (scope_ok and s == expected)]
|
||||
if extras:
|
||||
name, gid = os.environ[\"_SG_NAME\"], os.environ[\"_SG_ID\"]
|
||||
print(f\" WARN — security group {name} ({gid}) has ingress beyond the intended rule\", file=sys.stderr)
|
||||
print(f\" (tcp {port} from {expected}) — review it; remove anything you did not add deliberately:\", file=sys.stderr)
|
||||
for proto, lo, hi, src in extras:
|
||||
scope = \"all traffic\" if proto == \"-1\" else (f\"{proto} {lo}\" if lo == hi else f\"{proto} {lo}-{hi}\")
|
||||
print(f\" {scope} from {src}\", file=sys.stderr)
|
||||
" <<<"${perms}" || echo " WARN — could not audit ingress rules on $2 ($1)." >&2
|
||||
}
|
||||
|
||||
secret_arn() {
|
||||
aws secretsmanager describe-secret --secret-id "$1" \
|
||||
--query ARN --output text 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Existence check that fails closed: 0 = exists, 1 = definitively absent
|
||||
# (ResourceNotFoundException), anything else ABORTS the run. Gating on a bare
|
||||
# exit status would let a transient failure (throttle, expired token, network
|
||||
# blip) masquerade as "secret missing" — and the missing-secret branches below
|
||||
# do destructive work (the §3 self-heal resets the DB password), so they must
|
||||
# run only on a definitive not-found.
|
||||
secret_exists() { # <secret-id>
|
||||
local out
|
||||
if out="$(aws secretsmanager describe-secret --secret-id "$1" 2>&1 >/dev/null)"; then
|
||||
return 0
|
||||
elif grep -q 'ResourceNotFoundException' <<<"${out}"; then
|
||||
return 1
|
||||
else
|
||||
echo "ERROR: could not determine whether secret $1 exists (transient AWS error?):" >&2
|
||||
printf '%s\n' "${out}" >&2
|
||||
echo " Refusing to guess — re-run once the call succeeds." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Secret values must never appear on a process argv (argv is world-readable
|
||||
# via /proc and routinely recorded by EDR/auditd), so every aws call that
|
||||
# carries one takes it via --cli-input-json file://<0600 temp file> instead —
|
||||
# explicit flags on the same command line override/merge with the JSON, so
|
||||
# only the secret parameter needs to live in the file. secret_json writes
|
||||
# {"<Key>": "<value>"} to a fresh temp file and returns the path in the named
|
||||
# variable (printf -v, not command substitution — a subshell would lose the
|
||||
# SECRET_TMP_FILES bookkeeping below): the value crosses into python3 via the
|
||||
# environment (never argv) and json.dumps escapes it, so any characters
|
||||
# survive. Callers rm -f the file as soon as the aws call returns; the EXIT
|
||||
# trap sweeps whatever an aborted run leaves.
|
||||
SECRET_TMP_FILES=()
|
||||
cleanup_secret_tmp() { rm -f "${SECRET_TMP_FILES[@]+"${SECRET_TMP_FILES[@]}"}"; }
|
||||
trap cleanup_secret_tmp EXIT
|
||||
secret_json() { # secret_json <outvar> <JsonKey> <value> -> path in <outvar>
|
||||
local file
|
||||
file="$(mktemp)" # mktemp creates 0600
|
||||
chmod 600 "${file}" # belt and braces if TMPDIR overrides umask semantics
|
||||
SECRET_TMP_FILES+=("${file}")
|
||||
_JSON_KEY="$2" _JSON_VALUE="$3" python3 -c \
|
||||
'import json, os; print(json.dumps({os.environ["_JSON_KEY"]: os.environ["_JSON_VALUE"]}))' \
|
||||
> "${file}"
|
||||
printf -v "$1" '%s' "${file}"
|
||||
}
|
||||
|
||||
for required in AWS_REGION ACCOUNT_ID VPC_ID PRIVATE_SUBNETS CORP_CIDR VERSION; do
|
||||
if [[ -z "${!required}" ]]; then
|
||||
echo "ERROR: ${required} is not set." >&2
|
||||
case "${required}" in
|
||||
AWS_REGION) echo " Set it to a region where Bedrock serves the Claude models you need, e.g. export AWS_REGION=us-east-1" >&2 ;;
|
||||
ACCOUNT_ID) echo " Could not resolve it from STS — is the AWS CLI authenticated? (aws sts get-caller-identity)" >&2 ;;
|
||||
VPC_ID) echo " Set it to the VPC from the prerequisites, e.g. export VPC_ID=vpc-..." >&2 ;;
|
||||
PRIVATE_SUBNETS) echo " Set it to two+ private subnet IDs in different AZs, e.g. export PRIVATE_SUBNETS='subnet-a subnet-b'" >&2 ;;
|
||||
CORP_CIDR) echo " Set it to your corporate network CIDR (the ALB's :443 ingress source), e.g. export CORP_CIDR=10.0.0.0/8" >&2 ;;
|
||||
VERSION) echo " Set it to the gateway release version — it tags the image you build and push, e.g. export VERSION=<version>" >&2 ;;
|
||||
esac
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# The walkthrough (and this bundle) is scoped to commercial US regions: the
|
||||
# task role's Bedrock policy (§2) and the gateway's built-in model catalog
|
||||
# both use the us.anthropic.* geo-prefixed cross-region inference profiles,
|
||||
# which only exist in the commercial US regions — an explicit list, not a
|
||||
# `us-*` prefix match, because GovCloud (us-gov-*) and ISO (us-iso-*) regions
|
||||
# share the prefix but live in different AWS partitions where those profiles
|
||||
# and this bundle's arn:aws: ARNs are wrong. Anywhere else the deploy
|
||||
# provisions fine and then every model call fails. Other-region deploys must
|
||||
# pin region-appropriate inference profiles via a models: block in
|
||||
# gateway.yaml (see the config reference:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-config) and adjust the
|
||||
# inference-profile ARN prefix in bedrock-invoke.iam.json below — set
|
||||
# ALLOW_NON_US_REGION=1 once that's done to proceed.
|
||||
case "${AWS_REGION}" in
|
||||
us-east-1|us-east-2|us-west-1|us-west-2) ;;
|
||||
*)
|
||||
if [[ "${ALLOW_NON_US_REGION:-0}" != "1" ]]; then
|
||||
echo "ERROR: AWS_REGION=${AWS_REGION} is not a commercial US region, but this bundle's IAM policy" >&2
|
||||
echo " and model IDs use the US-geo (us.anthropic.*) cross-region inference profiles" >&2
|
||||
echo " (GovCloud/ISO regions are different partitions — the profiles and arn:aws: ARNs" >&2
|
||||
echo " here do not exist there)." >&2
|
||||
echo " Either deploy to us-east-1/us-east-2/us-west-1/us-west-2, or pin region-appropriate" >&2
|
||||
echo " inference profiles in a models: block in gateway.yaml" >&2
|
||||
echo " (https://code.claude.com/docs/en/claude-apps-gateway-config), adjust the" >&2
|
||||
echo " inference-profile ARN in the bedrock-invoke policy, and re-run with" >&2
|
||||
echo " ALLOW_NON_US_REGION=1." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "ERROR: python3 is required (it JSON-escapes secret values for --cli-input-json; the AWS CLI itself ships on Python)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086 # PRIVATE_SUBNETS is intentionally word-split everywhere below
|
||||
set -- ${PRIVATE_SUBNETS}
|
||||
if (( $# < 2 )); then
|
||||
echo "ERROR: PRIVATE_SUBNETS must list at least two subnets in different AZs (the internal ALB requires two)." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Normalize whatever whitespace (spaces, tabs, newlines) separates the list —
|
||||
# `set --` above word-split on IFS, so join those same words with commas
|
||||
# rather than only converting single spaces.
|
||||
SUBNETS_CSV="$(printf '%s,' "$@")"; SUBNETS_CSV="${SUBNETS_CSV%,}"
|
||||
|
||||
log "Account: ${ACCOUNT_ID} Region: ${AWS_REGION} VPC: ${VPC_ID}"
|
||||
|
||||
# ---- 1 Security groups -----------------------------------------------------
|
||||
# Three groups chain the traffic path (walkthrough §1): corp network -> ALB :443,
|
||||
# ALB -> gateway :8080, gateway -> Postgres :5432. Nothing else is reachable.
|
||||
log "Creating security groups (§1)"
|
||||
ALB_SG="$(sg_id "${ALB_SG_NAME}")"
|
||||
if [[ "${ALB_SG}" != "None" ]]; then
|
||||
skip "security group ${ALB_SG_NAME} (${ALB_SG})"
|
||||
else
|
||||
ALB_SG="$(aws ec2 create-security-group --group-name "${ALB_SG_NAME}" \
|
||||
--description "Claude gateway ALB" --vpc-id "${VPC_ID}" \
|
||||
--query GroupId --output text)"
|
||||
fi
|
||||
|
||||
GW_SG="$(sg_id "${GW_SG_NAME}")"
|
||||
if [[ "${GW_SG}" != "None" ]]; then
|
||||
skip "security group ${GW_SG_NAME} (${GW_SG})"
|
||||
else
|
||||
GW_SG="$(aws ec2 create-security-group --group-name "${GW_SG_NAME}" \
|
||||
--description "Claude gateway service" --vpc-id "${VPC_ID}" \
|
||||
--query GroupId --output text)"
|
||||
fi
|
||||
|
||||
DB_SG="$(sg_id "${DB_SG_NAME}")"
|
||||
if [[ "${DB_SG}" != "None" ]]; then
|
||||
skip "security group ${DB_SG_NAME} (${DB_SG})"
|
||||
else
|
||||
DB_SG="$(aws ec2 create-security-group --group-name "${DB_SG_NAME}" \
|
||||
--description "Claude gateway Postgres" --vpc-id "${VPC_ID}" \
|
||||
--query GroupId --output text)"
|
||||
fi
|
||||
|
||||
authorize_ingress --group-id "${ALB_SG}" --protocol tcp --port 443 --cidr "${CORP_CIDR}"
|
||||
authorize_ingress --group-id "${GW_SG}" --protocol tcp --port 8080 --source-group "${ALB_SG}"
|
||||
authorize_ingress --group-id "${DB_SG}" --protocol tcp --port 5432 --source-group "${GW_SG}"
|
||||
|
||||
# Flag any ingress beyond the three rules above (pre-existing groups may carry more).
|
||||
warn_unexpected_ingress "${ALB_SG}" "${ALB_SG_NAME}" 443 "${CORP_CIDR}"
|
||||
warn_unexpected_ingress "${GW_SG}" "${GW_SG_NAME}" 8080 "${ALB_SG}"
|
||||
warn_unexpected_ingress "${DB_SG}" "${DB_SG_NAME}" 5432 "${GW_SG}"
|
||||
|
||||
# ---- 2 IAM roles ------------------------------------------------------------
|
||||
# Task role: the gateway's runtime identity — its ONLY permission is invoking
|
||||
# Claude models on Bedrock (the upstream's `auth: {}` resolves to this role via
|
||||
# the AWS default credential chain). The policy must cover both the cross-region
|
||||
# inference-profile ARNs and the underlying foundation-model ARNs.
|
||||
# Execution role: the ECS agent's identity — pulls the image from ECR and
|
||||
# injects the Secrets Manager values; the gateway never uses it.
|
||||
log "Creating IAM roles ${TASK_ROLE} + ${EXEC_ROLE} (§2)"
|
||||
cat > ecs-trust.iam.json <<'EOF'
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Principal": { "Service": "ecs-tasks.amazonaws.com" },
|
||||
"Action": "sts:AssumeRole"
|
||||
}]
|
||||
}
|
||||
EOF
|
||||
cat > bedrock-invoke.iam.json <<EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
|
||||
"Resource": [
|
||||
"arn:aws:bedrock:${AWS_REGION}:${ACCOUNT_ID}:inference-profile/us.anthropic.*",
|
||||
"arn:aws:bedrock:*::foundation-model/anthropic.*"
|
||||
]
|
||||
}]
|
||||
}
|
||||
EOF
|
||||
# One ARN per secret (never a bare gateway-* wildcard, which would also match
|
||||
# unrelated secrets in a shared account). The trailing -?????? matches exactly
|
||||
# the random 6-character suffix Secrets Manager appends to every secret's ARN
|
||||
# (AWS's documented pattern; a trailing -* would be a plain prefix glob and
|
||||
# also match longer names like ${SECRET_NAME}-prod) — the exact ARNs aren't
|
||||
# knowable here because the role is created before the secrets are.
|
||||
cat > secrets-read.iam.json <<EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Action": "secretsmanager:GetSecretValue",
|
||||
"Resource": [
|
||||
"arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:${JWT_SECRET_NAME}-??????",
|
||||
"arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:${OIDC_SECRET_NAME}-??????",
|
||||
"arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:${SECRET_NAME}-??????"
|
||||
]
|
||||
}]
|
||||
}
|
||||
EOF
|
||||
|
||||
if aws iam get-role --role-name "${TASK_ROLE}" >/dev/null 2>&1; then
|
||||
skip "role ${TASK_ROLE}"
|
||||
else
|
||||
aws iam create-role --role-name "${TASK_ROLE}" \
|
||||
--assume-role-policy-document file://ecs-trust.iam.json >/dev/null
|
||||
fi
|
||||
# put-role-policy is an upsert — safe to re-run (it also picks up region changes).
|
||||
aws iam put-role-policy --role-name "${TASK_ROLE}" \
|
||||
--policy-name bedrock-invoke --policy-document file://bedrock-invoke.iam.json
|
||||
|
||||
if aws iam get-role --role-name "${EXEC_ROLE}" >/dev/null 2>&1; then
|
||||
skip "role ${EXEC_ROLE}"
|
||||
else
|
||||
aws iam create-role --role-name "${EXEC_ROLE}" \
|
||||
--assume-role-policy-document file://ecs-trust.iam.json >/dev/null
|
||||
fi
|
||||
# attach-role-policy is idempotent (re-attaching is a no-op).
|
||||
aws iam attach-role-policy --role-name "${EXEC_ROLE}" \
|
||||
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
|
||||
aws iam put-role-policy --role-name "${EXEC_ROLE}" \
|
||||
--policy-name read-gateway-secrets --policy-document file://secrets-read.iam.json
|
||||
|
||||
echo " NOTE: Bedrock model access is console-only — enable it for the Claude models"
|
||||
echo " you need (Bedrock console -> Model access), and submit the one-time use"
|
||||
echo " case form for the account. Cross-region inference profiles"
|
||||
echo " (us.anthropic.*) need access in EACH region the profile spans."
|
||||
|
||||
# ---- 6 Build & push image to Amazon ECR (config baked in — §6 + §4) ---------
|
||||
log "Ensuring ECR repository and image (§6)"
|
||||
if aws ecr describe-repositories --repository-names "${ECR_REPO}" >/dev/null 2>&1; then
|
||||
skip "ECR repository ${ECR_REPO}"
|
||||
# Integrity-critical settings: converge on re-runs so a pre-existing MUTABLE repo can't slip through.
|
||||
aws ecr put-image-tag-mutability --repository-name "${ECR_REPO}" \
|
||||
--image-tag-mutability IMMUTABLE >/dev/null
|
||||
aws ecr put-image-scanning-configuration --repository-name "${ECR_REPO}" \
|
||||
--image-scanning-configuration scanOnPush=true >/dev/null
|
||||
else
|
||||
# IMMUTABLE tags + scan-on-push: the ECS service pulls whatever this repo
|
||||
# serves under the deployed tag, so a pushed tag must never be silently
|
||||
# re-pointed. For production, also restrict push rights on this repo to your
|
||||
# CI / image-promotion pipeline rather than operator credentials — this
|
||||
# walkthrough pushes directly for simplicity.
|
||||
aws ecr create-repository --repository-name "${ECR_REPO}" \
|
||||
--image-tag-mutability IMMUTABLE \
|
||||
--image-scanning-configuration scanOnPush=true >/dev/null
|
||||
fi
|
||||
|
||||
# The config is baked into the image, so the build is gated the way the GCP
|
||||
# example gates its config-secret publish: gateway.yaml must exist and be fully
|
||||
# filled in (REPLACE_ME checked on non-comment lines so commented examples and
|
||||
# the file's header don't trip the guard). The tag carries a hash of the config
|
||||
# so an edit produces a NEW tag (required by tag immutability) and a re-run
|
||||
# rebuilds automatically.
|
||||
IMAGE=""
|
||||
if [[ ! -f "${GATEWAY_YAML}" ]]; then
|
||||
echo " (skip) ${GATEWAY_YAML} not found — run 'cp gateway.yaml.example gateway.yaml', fill it in, then re-run (§4)."
|
||||
elif grep -vE '^[[:space:]]*#' "${GATEWAY_YAML}" | grep -q 'REPLACE_ME'; then
|
||||
echo " (skip) ${GATEWAY_YAML} still has REPLACE_ME placeholders to fill:"
|
||||
grep -nE 'REPLACE_ME' "${GATEWAY_YAML}" | grep -vE '^[0-9]+:[[:space:]]*#' | sed 's/^/ /'
|
||||
echo " Fill them in, then re-run to build the image (the config is baked in)."
|
||||
else
|
||||
CONFIG_SHA="$(sha_of "${GATEWAY_YAML}" | cut -c1-8)"
|
||||
IMAGE_TAG="${IMAGE_TAG:-${VERSION}-cfg${CONFIG_SHA}}"
|
||||
IMAGE="${REGISTRY}/${ECR_REPO}:${IMAGE_TAG}"
|
||||
|
||||
# Image is the expensive, already-done step: skip the build+push entirely if
|
||||
# the tag already exists in the registry.
|
||||
if aws ecr describe-images --repository-name "${ECR_REPO}" \
|
||||
--image-ids "imageTag=${IMAGE_TAG}" >/dev/null 2>&1; then
|
||||
skip "image ${IMAGE}"
|
||||
else
|
||||
# When the expected checksum is known, verify a PRE-EXISTING binary too:
|
||||
# the [[ ! -f ]] guard below otherwise trusts whatever is on disk, so a
|
||||
# stale binary from an earlier VERSION (or a tampered one) would be baked
|
||||
# into the image silently. On mismatch, set it aside (never delete — the
|
||||
# mismatch may be a typo'd DIST_SHA256, not a bad binary) and fall through
|
||||
# to the fail-closed download path. Without DIST_SHA256 the operator-
|
||||
# provided-binary flow is unchanged — no checksum was declared, so none is
|
||||
# checked.
|
||||
QUARANTINED_SHA=""
|
||||
if [[ -n "${DIST_SHA256}" && -f "${CLAUDE_BINARY}" ]]; then
|
||||
existing_sha="$(sha_of "${CLAUDE_BINARY}")"
|
||||
if [[ "${existing_sha}" != "${DIST_SHA256}" ]]; then
|
||||
log "Existing ${CLAUDE_BINARY} sha256 ${existing_sha} does not match DIST_SHA256 — setting it aside as ${CLAUDE_BINARY}.bad"
|
||||
mv -f "${CLAUDE_BINARY}" "${CLAUDE_BINARY}.bad"
|
||||
QUARANTINED_SHA="${existing_sha}"
|
||||
fi
|
||||
fi
|
||||
if [[ ! -f "${CLAUDE_BINARY}" ]]; then
|
||||
if [[ -n "${DIST_URL}" ]]; then
|
||||
# Fail closed: never download an executable we can't verify.
|
||||
if [[ -z "${DIST_SHA256}" ]]; then
|
||||
echo "ERROR: DIST_SHA256 must be set when DIST_URL is used — refusing to download an unverified binary." >&2
|
||||
echo " Set DIST_SHA256 to the expected sha256 of the binary at DIST_URL, obtained out-of-band:" >&2
|
||||
echo " for standard-release binaries, from the release's GPG-signed manifest.json (verify the" >&2
|
||||
echo " manifest signature first — see code.claude.com/docs/en/setup#binary-integrity-and-code-signing);" >&2
|
||||
echo " otherwise from the channel that published the download link, never from the download server." >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Downloading gateway binary from ${DIST_URL}"
|
||||
# Download to a temp path and only mv into place after the checksum
|
||||
# verifies, so an interrupted download can't leave a partial CLAUDE_BINARY
|
||||
# that the [[ ! -f ]] guard above would skip — and silently push — on re-run.
|
||||
# Refuse plaintext/protocol-downgrade; only follow HTTPS redirects.
|
||||
dl_tmp="${CLAUDE_BINARY}.download"
|
||||
rm -f "${dl_tmp}"
|
||||
curl_https -fL -o "${dl_tmp}" "${DIST_URL}"
|
||||
actual_sha="$(sha_of "${dl_tmp}")"
|
||||
if [[ "${actual_sha}" != "${DIST_SHA256}" ]]; then
|
||||
echo "ERROR: checksum mismatch for ${dl_tmp} (expected ${DIST_SHA256}, got ${actual_sha}) — refusing to build." >&2
|
||||
rm -f "${dl_tmp}"
|
||||
exit 1
|
||||
fi
|
||||
log "Verified binary sha256 ${actual_sha}"
|
||||
chmod +x "${dl_tmp}"
|
||||
mv -f "${dl_tmp}" "${CLAUDE_BINARY}"
|
||||
else
|
||||
echo "ERROR: build binary not found at ${CLAUDE_BINARY} and DIST_URL is not set." >&2
|
||||
if [[ -n "${QUARANTINED_SHA}" ]]; then
|
||||
echo " The binary that WAS there had sha256 ${QUARANTINED_SHA}, which does not match" >&2
|
||||
echo " DIST_SHA256=${DIST_SHA256} — it was preserved as ${CLAUDE_BINARY}.bad." >&2
|
||||
echo " If DIST_SHA256 was a typo, fix it and move the file back:" >&2
|
||||
echo " mv '${CLAUDE_BINARY}.bad' '${CLAUDE_BINARY}'" >&2
|
||||
echo " Otherwise treat that file as untrusted and obtain a verified binary." >&2
|
||||
fi
|
||||
echo " Provide the prebuilt linux-x64 Claude Code release binary at that path" >&2
|
||||
echo " or set DIST_URL to its download URL (see the walkthrough, §6)." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# The RDS CA bundle is baked into the image as the trust anchor for the
|
||||
# connection string's sslmode=verify-full (§3/§5). Fail closed: no bundle,
|
||||
# no build. AWS rotates the bundle, so no checksum is pinned (see the
|
||||
# RDS_CA_BUNDLE_URL comment up top); the sanity check below catches an
|
||||
# error page or truncated download.
|
||||
if [[ ! -f "${RDS_CA_BUNDLE}" ]]; then
|
||||
log "Downloading RDS CA bundle from ${RDS_CA_BUNDLE_URL}"
|
||||
curl_https -fL -o "${RDS_CA_BUNDLE}" "${RDS_CA_BUNDLE_URL}"
|
||||
fi
|
||||
if ! grep -q 'BEGIN CERTIFICATE' "${RDS_CA_BUNDLE}" \
|
||||
|| (( "$(wc -c < "${RDS_CA_BUNDLE}")" < 10000 )); then
|
||||
echo "ERROR: ${RDS_CA_BUNDLE} does not look like the RDS CA bundle (missing PEM blocks or implausibly small) — refusing to build." >&2
|
||||
echo " Delete it and re-run to re-download, or place the bundle from ${RDS_CA_BUNDLE_URL} there yourself." >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Building and pushing ${IMAGE}"
|
||||
aws ecr get-login-password --region "${AWS_REGION}" \
|
||||
| docker login --username AWS --password-stdin "${REGISTRY}"
|
||||
# The task definition below runs linux/amd64 (cpuArchitecture X86_64);
|
||||
# --platform forces it (e.g. when building on an Apple Silicon Mac), and
|
||||
# --provenance=false keeps buildx from wrapping the result in an OCI image
|
||||
# index that some pullers reject. For Fargate on ARM64 (Graviton), build
|
||||
# linux/arm64 with the linux-arm64 binary and set cpuArchitecture to ARM64.
|
||||
docker build --platform=linux/amd64 --provenance=false \
|
||||
-f "${DOCKERFILE}" \
|
||||
--build-arg CLAUDE_BINARY="${CLAUDE_BINARY}" \
|
||||
--build-arg GATEWAY_CONFIG="${GATEWAY_YAML}" \
|
||||
--build-arg RDS_CA_BUNDLE="${RDS_CA_BUNDLE}" \
|
||||
-t "${IMAGE}" .
|
||||
docker push "${IMAGE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 3 RDS for PostgreSQL (private subnets, no public address) --------------
|
||||
log "Creating DB subnet group ${DB_SUBNET_GROUP} (§3)"
|
||||
if aws rds describe-db-subnet-groups --db-subnet-group-name "${DB_SUBNET_GROUP}" >/dev/null 2>&1; then
|
||||
skip "DB subnet group ${DB_SUBNET_GROUP}"
|
||||
else
|
||||
# shellcheck disable=SC2086 # subnet IDs are separate arguments by design
|
||||
aws rds create-db-subnet-group --db-subnet-group-name "${DB_SUBNET_GROUP}" \
|
||||
--db-subnet-group-description "Claude gateway" --subnet-ids ${PRIVATE_SUBNETS} >/dev/null
|
||||
fi
|
||||
|
||||
# Parameter group with rds.force_ssl=1: the server side of TLS enforcement —
|
||||
# the client side is sslmode=verify-full in the connection string (§5). The
|
||||
# family must match the engine major version, so it derives from the same
|
||||
# DB_ENGINE_VERSION that create-db-instance pins below.
|
||||
log "Ensuring DB parameter group ${DB_PARAM_GROUP} (rds.force_ssl=1)"
|
||||
PG_FAMILY="postgres${DB_ENGINE_VERSION%%.*}"
|
||||
if aws rds describe-db-parameter-groups --db-parameter-group-name "${DB_PARAM_GROUP}" >/dev/null 2>&1; then
|
||||
skip "DB parameter group ${DB_PARAM_GROUP}"
|
||||
else
|
||||
aws rds create-db-parameter-group --db-parameter-group-name "${DB_PARAM_GROUP}" \
|
||||
--db-parameter-group-family "${PG_FAMILY}" \
|
||||
--description "Claude gateway - require TLS on every connection" >/dev/null
|
||||
fi
|
||||
# modify-db-parameter-group is an upsert — applied every run so a pre-existing
|
||||
# group converges too. rds.force_ssl is dynamic; no reboot needed.
|
||||
aws rds modify-db-parameter-group --db-parameter-group-name "${DB_PARAM_GROUP}" \
|
||||
--parameters "ParameterName=rds.force_ssl,ParameterValue=1,ApplyMethod=immediate" >/dev/null
|
||||
|
||||
# hex (not base64) keeps the password URL-safe for the connection string below.
|
||||
# The password reaches every aws call via --cli-input-json (never argv — see
|
||||
# the secret_json helper); explicit flags merge with (and would override) the
|
||||
# JSON, so only the password lives in the temp file.
|
||||
log "Creating RDS instance ${DB_INSTANCE} (private subnets, --no-publicly-accessible)"
|
||||
DB_PASSWORD=""
|
||||
DB_POSTURE="$(aws rds describe-db-instances --db-instance-identifier "${DB_INSTANCE}" \
|
||||
--query 'DBInstances[0].[PubliclyAccessible,StorageEncrypted]' --output text 2>/dev/null || true)"
|
||||
if [[ -n "${DB_POSTURE}" ]]; then
|
||||
# Name-based reuse: a pre-existing instance may not carry the posture this
|
||||
# script would have created it with. Non-fatal (the operator may be migrating
|
||||
# an existing DB on purpose), but drift from the guide's baseline must be seen.
|
||||
read -r DB_PUBLIC DB_ENCRYPTED <<<"${DB_POSTURE}"
|
||||
if [[ "${DB_PUBLIC}" == "True" ]]; then
|
||||
echo " WARN — RDS instance ${DB_INSTANCE} is PubliclyAccessible; this script would have" >&2
|
||||
echo " created it with --no-publicly-accessible. Fix: aws rds modify-db-instance" >&2
|
||||
echo " --db-instance-identifier ${DB_INSTANCE} --no-publicly-accessible --apply-immediately" >&2
|
||||
fi
|
||||
if [[ "${DB_ENCRYPTED}" == "False" ]]; then
|
||||
echo " WARN — RDS instance ${DB_INSTANCE} has StorageEncrypted=false; this script would" >&2
|
||||
echo " have created it with --storage-encrypted (encryption cannot be enabled in" >&2
|
||||
echo " place — restore an encrypted snapshot copy to migrate)." >&2
|
||||
fi
|
||||
if secret_exists "${SECRET_NAME}"; then
|
||||
skip "instance ${DB_INSTANCE} (password unchanged; secret not rewritten)"
|
||||
else
|
||||
# Self-heal: a previous run died after creating the instance but before
|
||||
# writing the connection-string secret, losing the only copy of the
|
||||
# password. The secret is the password's only consumer, so resetting it is
|
||||
# safe and keeps re-runs able to recover from any partial state.
|
||||
# secret_exists (not a bare exit-status check) gates this: only a
|
||||
# definitive ResourceNotFoundException may trigger a password reset.
|
||||
# ORDERING INVARIANT: the secret write (§5 below) is the heal's commit
|
||||
# point — everything that can fail must happen BEFORE it, so a crash at
|
||||
# any point leaves the secret still missing and the next run simply
|
||||
# repeats the heal. Writing the secret first would invert that: a crash
|
||||
# between secret write and modify-db-instance would leave an existing
|
||||
# secret whose password the DB never received, and every later run would
|
||||
# skip the heal while the gateway can't connect.
|
||||
# NOTE: the parameter group is attached on create only — an instance that
|
||||
# predates it keeps its current group (attach via modify-db-instance
|
||||
# --db-parameter-group-name yourself if you want force_ssl retrofitted).
|
||||
log "Instance ${DB_INSTANCE} exists but secret ${SECRET_NAME} is missing — resetting password"
|
||||
DB_PASSWORD="$(openssl rand -hex 24)"
|
||||
pw_json=""; secret_json pw_json MasterUserPassword "${DB_PASSWORD}"
|
||||
aws rds modify-db-instance --db-instance-identifier "${DB_INSTANCE}" \
|
||||
--cli-input-json "file://${pw_json}" --apply-immediately >/dev/null
|
||||
rm -f "${pw_json}"
|
||||
fi
|
||||
else
|
||||
DB_PASSWORD="$(openssl rand -hex 24)"
|
||||
pw_json=""; secret_json pw_json MasterUserPassword "${DB_PASSWORD}"
|
||||
aws rds create-db-instance --db-instance-identifier "${DB_INSTANCE}" \
|
||||
--engine postgres --engine-version "${DB_ENGINE_VERSION}" \
|
||||
--db-instance-class "${DB_CLASS}" \
|
||||
--allocated-storage "${DB_STORAGE_GB}" --db-name "${DB_NAME}" \
|
||||
--master-username "${DB_USER}" --cli-input-json "file://${pw_json}" \
|
||||
--db-subnet-group-name "${DB_SUBNET_GROUP}" \
|
||||
--db-parameter-group-name "${DB_PARAM_GROUP}" \
|
||||
--vpc-security-group-ids "${DB_SG}" \
|
||||
--no-publicly-accessible \
|
||||
--storage-encrypted >/dev/null
|
||||
rm -f "${pw_json}"
|
||||
fi
|
||||
|
||||
log "Waiting for ${DB_INSTANCE} to become available (first creation takes ~10 min)"
|
||||
aws rds wait db-instance-available --db-instance-identifier "${DB_INSTANCE}"
|
||||
DB_HOST="$(aws rds describe-db-instances --db-instance-identifier "${DB_INSTANCE}" \
|
||||
--query 'DBInstances[0].Endpoint.Address' --output text)"
|
||||
|
||||
# ---- 5 Connection string + JWT secret -> Secrets Manager --------------------
|
||||
# No per-secret IAM grants are needed: the execution role's read-gateway-secrets
|
||||
# policy (§2) names each of the three secrets by its ARN prefix.
|
||||
# Secret values go to aws via --cli-input-json temp files, never argv.
|
||||
if [[ -n "${DB_PASSWORD}" ]]; then
|
||||
# RDS private endpoint (guide §3); the gateway connects directly over the
|
||||
# VPC — the DB security group only admits ${GW_SG_NAME}.
|
||||
# sslmode=verify-full: the gateway's driver honors sslmode from the URL and
|
||||
# verifies the RDS certificate chain AND hostname against the CA bundle the
|
||||
# image trusts via NODE_EXTRA_CA_CERTS (see the Dockerfile). Do NOT add a
|
||||
# libpq-style `sslrootcert=` query param — the driver doesn't read it and
|
||||
# forwards it to Postgres as a startup parameter, which the server rejects.
|
||||
CONN="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/${DB_NAME}?sslmode=verify-full"
|
||||
log "Storing connection string in Secrets Manager secret ${SECRET_NAME} (§5)"
|
||||
conn_json=""; secret_json conn_json SecretString "${CONN}"
|
||||
if secret_exists "${SECRET_NAME}"; then
|
||||
aws secretsmanager put-secret-value --secret-id "${SECRET_NAME}" \
|
||||
--cli-input-json "file://${conn_json}" >/dev/null
|
||||
else
|
||||
aws secretsmanager create-secret --name "${SECRET_NAME}" \
|
||||
--cli-input-json "file://${conn_json}" >/dev/null
|
||||
fi
|
||||
rm -f "${conn_json}"
|
||||
else
|
||||
log "Skipping postgres-url secret write (instance already existed, password not available this run)"
|
||||
fi
|
||||
|
||||
# JWT signing secret — generated once (re-runs do NOT rotate it).
|
||||
log "Ensuring JWT signing secret ${JWT_SECRET_NAME} (§5)"
|
||||
if secret_exists "${JWT_SECRET_NAME}"; then
|
||||
skip "secret ${JWT_SECRET_NAME}"
|
||||
else
|
||||
jwt_json=""; secret_json jwt_json SecretString "$(openssl rand -base64 32)"
|
||||
aws secretsmanager create-secret --name "${JWT_SECRET_NAME}" \
|
||||
--cli-input-json "file://${jwt_json}" >/dev/null
|
||||
rm -f "${jwt_json}"
|
||||
fi
|
||||
|
||||
# OIDC client secret — operator-created (the script can't generate it; it comes
|
||||
# from the Okta OIDC web application). Checked here so the deploy step below can
|
||||
# gate on it with a clear message instead of a raw ECS secret-injection failure.
|
||||
OIDC_ARN="$(secret_arn "${OIDC_SECRET_NAME}")"
|
||||
|
||||
# ---- 7 ECS Fargate service + internal ALB ----------------------------------
|
||||
# Self-gating: deploy only once its inputs exist (image pushed — i.e.
|
||||
# gateway.yaml was filled in — plus the operator-provided OIDC client secret
|
||||
# and the ACM certificate for the internal hostname). On a first run these are
|
||||
# usually missing and it cleanly skips.
|
||||
ALB_DNS=""
|
||||
missing=""
|
||||
[[ -n "${IMAGE}" ]] || missing="${missing} image(fill ${GATEWAY_YAML})"
|
||||
[[ -n "${OIDC_ARN}" ]] || missing="${missing} ${OIDC_SECRET_NAME}"
|
||||
[[ -n "${ACM_CERT_ARN}" ]] || missing="${missing} ACM_CERT_ARN"
|
||||
SECRET_ARN="$(secret_arn "${SECRET_NAME}")"
|
||||
JWT_ARN="$(secret_arn "${JWT_SECRET_NAME}")"
|
||||
[[ -n "${SECRET_ARN}" ]] || missing="${missing} ${SECRET_NAME}"
|
||||
[[ -n "${JWT_ARN}" ]] || missing="${missing} ${JWT_SECRET_NAME}"
|
||||
|
||||
if [[ "${DEPLOY}" != "1" ]]; then
|
||||
log "Skipping ECS/ALB deploy (DEPLOY=${DEPLOY}) (§7)"
|
||||
elif [[ -n "${missing// }" ]]; then
|
||||
log "Skipping ECS/ALB deploy — missing input(s):${missing} (§7)"
|
||||
echo " Fill ${GATEWAY_YAML} and re-run to build the image; create ${OIDC_SECRET_NAME}"
|
||||
echo " from the Okta client secret; set ACM_CERT_ARN to the certificate for your"
|
||||
echo " internal gateway hostname. Then re-run to deploy."
|
||||
else
|
||||
log "Creating ECS cluster ${CLUSTER} and log group ${LOG_GROUP} (§7)"
|
||||
if [[ "$(aws ecs describe-clusters --clusters "${CLUSTER}" \
|
||||
--query 'clusters[0].status' --output text 2>/dev/null)" == "ACTIVE" ]]; then
|
||||
skip "cluster ${CLUSTER}"
|
||||
else
|
||||
aws ecs create-cluster --cluster-name "${CLUSTER}" >/dev/null
|
||||
fi
|
||||
# The gateway's stderr carries both its audit events and operational logs.
|
||||
if aws logs describe-log-groups --log-group-name-prefix "${LOG_GROUP}" \
|
||||
--query 'logGroups[?logGroupName==`'"${LOG_GROUP}"'`]' --output text 2>/dev/null | grep -q .; then
|
||||
skip "log group ${LOG_GROUP}"
|
||||
else
|
||||
aws logs create-log-group --log-group-name "${LOG_GROUP}"
|
||||
fi
|
||||
# Retention is a separate API (create-log-group has no retention flag) and an
|
||||
# upsert — applied every run so pre-existing groups converge too. Without it
|
||||
# the group keeps logs forever and cost grows unbounded.
|
||||
aws logs put-retention-policy --log-group-name "${LOG_GROUP}" \
|
||||
--retention-in-days "${LOG_RETENTION_DAYS}"
|
||||
|
||||
# Task definition: the task role carries the Bedrock permission; the
|
||||
# execution role injects the secrets. Registering is an append (a new
|
||||
# revision) — the service below always points at the latest.
|
||||
log "Registering task definition ${TASK_FAMILY}"
|
||||
taskdef_tmp="$(mktemp)"
|
||||
cat > "${taskdef_tmp}" <<EOF
|
||||
{
|
||||
"family": "${TASK_FAMILY}",
|
||||
"networkMode": "awsvpc",
|
||||
"requiresCompatibilities": ["FARGATE"],
|
||||
"cpu": "${TASK_CPU}",
|
||||
"memory": "${TASK_MEMORY}",
|
||||
"runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" },
|
||||
"executionRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/${EXEC_ROLE}",
|
||||
"taskRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/${TASK_ROLE}",
|
||||
"containerDefinitions": [
|
||||
{
|
||||
"name": "gateway",
|
||||
"image": "${IMAGE}",
|
||||
"portMappings": [{ "containerPort": 8080 }],
|
||||
"secrets": [
|
||||
{ "name": "GATEWAY_JWT_SECRET", "valueFrom": "${JWT_ARN}" },
|
||||
{ "name": "OIDC_CLIENT_SECRET", "valueFrom": "${OIDC_ARN}" },
|
||||
{ "name": "GATEWAY_POSTGRES_URL", "valueFrom": "${SECRET_ARN}" }
|
||||
],
|
||||
"logConfiguration": {
|
||||
"logDriver": "awslogs",
|
||||
"options": {
|
||||
"awslogs-group": "${LOG_GROUP}",
|
||||
"awslogs-region": "${AWS_REGION}",
|
||||
"awslogs-stream-prefix": "gateway"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
aws ecs register-task-definition --cli-input-json "file://${taskdef_tmp}" >/dev/null
|
||||
rm -f "${taskdef_tmp}"
|
||||
|
||||
# Internal ALB. --ip-address-type ipv4: an internal dual-stack ALB publishes
|
||||
# public-range AAAA records, which the CLI's /login private-network check
|
||||
# rejects.
|
||||
log "Creating internal ALB ${ALB_NAME} + target group + HTTPS listener"
|
||||
read -r ALB_ARN ALB_SCHEME ALB_VPC ALB_IP_TYPE <<<"$(aws elbv2 describe-load-balancers --names "${ALB_NAME}" \
|
||||
--query 'LoadBalancers[0].[LoadBalancerArn,Scheme,VpcId,IpAddressType]' --output text 2>/dev/null || true)"
|
||||
if [[ -n "${ALB_ARN}" && "${ALB_ARN}" != "None" ]]; then
|
||||
# Reuse is by name, and scheme/VPC are immutable on an ALB — so posture is
|
||||
# asserted, fail-closed: attaching the gateway to an internet-facing or
|
||||
# wrong-VPC load balancer would change the exposure model, not just drift.
|
||||
if [[ "${ALB_SCHEME}" != "internal" || "${ALB_VPC}" != "${VPC_ID}" ]]; then
|
||||
echo "ERROR: load balancer ${ALB_NAME} exists but is not the internal ALB this script expects:" >&2
|
||||
echo " scheme=${ALB_SCHEME} (need internal), vpc=${ALB_VPC} (need ${VPC_ID})." >&2
|
||||
echo " Refusing to deploy the gateway behind it. Delete that load balancer, or set" >&2
|
||||
echo " ALB_NAME to an unused name, then re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
skip "load balancer ${ALB_NAME} (internal, ${ALB_VPC})"
|
||||
# ip-address-type IS mutable (unlike scheme/VPC) — converge a reused
|
||||
# dualstack ALB back to ipv4, matching the Terraform sibling: dual-stack
|
||||
# publishes public-range AAAA records that /login rejects (see above).
|
||||
if [[ "${ALB_IP_TYPE}" != "ipv4" ]]; then
|
||||
aws elbv2 set-ip-address-type --load-balancer-arn "${ALB_ARN}" \
|
||||
--ip-address-type ipv4 >/dev/null
|
||||
fi
|
||||
else
|
||||
# shellcheck disable=SC2086
|
||||
ALB_ARN="$(aws elbv2 create-load-balancer --name "${ALB_NAME}" \
|
||||
--scheme internal --type application --ip-address-type ipv4 \
|
||||
--subnets ${PRIVATE_SUBNETS} --security-groups "${ALB_SG}" \
|
||||
--query 'LoadBalancers[0].LoadBalancerArn' --output text)"
|
||||
fi
|
||||
|
||||
# The ALB closes a connection after 60 seconds with no data by default, which
|
||||
# cuts off streams during quiet periods (long prompt processing before the
|
||||
# first token, extended thinking). Attribute setting is idempotent.
|
||||
aws elbv2 modify-load-balancer-attributes --load-balancer-arn "${ALB_ARN}" \
|
||||
--attributes Key=idle_timeout.timeout_seconds,Value=3600 >/dev/null
|
||||
|
||||
read -r TG_ARN TG_VPC <<<"$(aws elbv2 describe-target-groups --names "${TG_NAME}" \
|
||||
--query 'TargetGroups[0].[TargetGroupArn,VpcId]' --output text 2>/dev/null || true)"
|
||||
if [[ -n "${TG_ARN}" && "${TG_ARN}" != "None" ]]; then
|
||||
skip "target group ${TG_NAME}"
|
||||
# VPC is immutable on a target group; a wrong-VPC one can't reach the tasks.
|
||||
if [[ "${TG_VPC}" != "${VPC_ID}" ]]; then
|
||||
echo " WARN — target group ${TG_NAME} is in ${TG_VPC}, not ${VPC_ID}; the service's tasks" >&2
|
||||
echo " will not become healthy behind it. Delete it or set TG_NAME to an unused" >&2
|
||||
echo " name, then re-run." >&2
|
||||
fi
|
||||
else
|
||||
# /readyz verifies the store is reachable, so a task that can't reach
|
||||
# Postgres never enters rotation (the gateway also serves liveness-only
|
||||
# /healthz — see the deploy guide's outage-behavior tradeoff).
|
||||
TG_ARN="$(aws elbv2 create-target-group --name "${TG_NAME}" \
|
||||
--protocol HTTP --port 8080 --vpc-id "${VPC_ID}" --target-type ip \
|
||||
--health-check-path /readyz \
|
||||
--query 'TargetGroups[0].TargetGroupArn' --output text)"
|
||||
fi
|
||||
|
||||
# Select the HTTPS:443 listener specifically — a reused ALB may carry other
|
||||
# listeners (say HTTP:80); those stay untouched, and the 443 listener is
|
||||
# still created when it's the one that's missing.
|
||||
# shellcheck disable=SC2016 # backticks are JMESPath literals, not expansion
|
||||
LISTENER_ARN="$(aws elbv2 describe-listeners --load-balancer-arn "${ALB_ARN}" \
|
||||
--query 'Listeners[?Port==`443`]|[0].ListenerArn' --output text 2>/dev/null || true)"
|
||||
if [[ -n "${LISTENER_ARN}" && "${LISTENER_ARN}" != "None" ]]; then
|
||||
skip "HTTPS:443 listener on ${ALB_NAME}"
|
||||
# Converge everything this script owns on pre-existing listeners
|
||||
# (modify-listener is an upsert): the TLS policy (so re-runs pick up an
|
||||
# ALB_SSL_POLICY change, and listeners created before this script pinned
|
||||
# one lose the legacy default), the certificate (so a changed ACM_CERT_ARN
|
||||
# — e.g. a renewal under a new ARN — is not silently ignored), and the
|
||||
# default action (so the listener always forwards to this target group).
|
||||
aws elbv2 modify-listener --listener-arn "${LISTENER_ARN}" \
|
||||
--ssl-policy "${ALB_SSL_POLICY}" \
|
||||
--certificates "CertificateArn=${ACM_CERT_ARN}" \
|
||||
--default-actions "Type=forward,TargetGroupArn=${TG_ARN}" >/dev/null
|
||||
else
|
||||
aws elbv2 create-listener --load-balancer-arn "${ALB_ARN}" \
|
||||
--protocol HTTPS --port 443 \
|
||||
--ssl-policy "${ALB_SSL_POLICY}" \
|
||||
--certificates "CertificateArn=${ACM_CERT_ARN}" \
|
||||
--default-actions "Type=forward,TargetGroupArn=${TG_ARN}" >/dev/null
|
||||
fi
|
||||
|
||||
# Service: created once, then rolled forward — a re-run points it at the
|
||||
# latest task-definition revision (which carries the current image tag, and
|
||||
# therefore the current gateway.yaml) and forces a new deployment.
|
||||
log "Creating/updating ECS service ${SERVICE} (Fargate, private subnets, no public IP)"
|
||||
svc_status="$(aws ecs describe-services --cluster "${CLUSTER}" --services "${SERVICE}" \
|
||||
--query 'services[0].status' --output text 2>/dev/null || true)"
|
||||
if [[ "${svc_status}" == "ACTIVE" ]]; then
|
||||
aws ecs update-service --cluster "${CLUSTER}" --service "${SERVICE}" \
|
||||
--task-definition "${TASK_FAMILY}" --desired-count "${DESIRED_COUNT}" \
|
||||
--deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true}" \
|
||||
--health-check-grace-period-seconds 60 \
|
||||
--force-new-deployment >/dev/null
|
||||
echo " service updated to the latest task-definition revision."
|
||||
else
|
||||
# All egress (Bedrock, the IdP, Secrets Manager, ECR, CloudWatch Logs) goes
|
||||
# through the NAT gateway — assignPublicIp stays DISABLED.
|
||||
# The deployment circuit breaker stops a rollout whose tasks keep failing
|
||||
# (bad image, unbootable config) and rolls back to the last steady state
|
||||
# instead of relaunching failing tasks forever. The health-check grace
|
||||
# period gives a cold task (image pull + store connect + first /readyz)
|
||||
# time before ECS counts it unhealthy — without it the circuit breaker can
|
||||
# declare the very first rollout failed (matches terraform/'s
|
||||
# health_check_grace_period_seconds).
|
||||
aws ecs create-service --cluster "${CLUSTER}" --service-name "${SERVICE}" \
|
||||
--task-definition "${TASK_FAMILY}" --desired-count "${DESIRED_COUNT}" \
|
||||
--launch-type FARGATE \
|
||||
--deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true}" \
|
||||
--health-check-grace-period-seconds 60 \
|
||||
--network-configuration "awsvpcConfiguration={subnets=[${SUBNETS_CSV}],securityGroups=[${GW_SG}],assignPublicIp=DISABLED}" \
|
||||
--load-balancers "targetGroupArn=${TG_ARN},containerName=gateway,containerPort=8080" >/dev/null
|
||||
fi
|
||||
|
||||
ALB_DNS="$(aws elbv2 describe-load-balancers --load-balancer-arns "${ALB_ARN}" \
|
||||
--query 'LoadBalancers[0].DNSName' --output text)"
|
||||
log "Internal ALB DNS: ${ALB_DNS}"
|
||||
|
||||
# Post-deploy smoke check: the ALB is internal (unreachable from this
|
||||
# machine), but target health is visible through the API — poll until the
|
||||
# /readyz health check passes. Non-fatal; a cold task needs a minute or two
|
||||
# (image pull + store connect).
|
||||
log "Smoke check: polling target health on ${TG_NAME} (health check: GET /readyz)"
|
||||
tg_state="unknown"
|
||||
for _ in $(seq 1 24); do
|
||||
tg_state="$(aws elbv2 describe-target-health --target-group-arn "${TG_ARN}" \
|
||||
--query 'TargetHealthDescriptions[0].TargetHealth.State' --output text 2>/dev/null || true)"
|
||||
[[ "${tg_state}" == "healthy" ]] && break
|
||||
sleep 10
|
||||
done
|
||||
if [[ "${tg_state}" == "healthy" ]]; then
|
||||
echo " OK — a gateway task is healthy behind the ALB (store reachable)."
|
||||
else
|
||||
echo " WARN — last target state: ${tg_state:-none}; the task may still be starting."
|
||||
echo " Check the service events and the gateway's logs:"
|
||||
echo " aws ecs describe-services --cluster ${CLUSTER} --services ${SERVICE} --query 'services[0].events[:5]'"
|
||||
echo " aws logs tail ${LOG_GROUP} --since 10m"
|
||||
fi
|
||||
|
||||
# public_url is baked into the image, so verify the operator's chosen
|
||||
# hostname is in place (the redirect URI and discovery doc derive from it).
|
||||
CFG_PUBLIC_URL="$(grep -E '^[[:space:]]*public_url:' "${GATEWAY_YAML}" 2>/dev/null \
|
||||
| head -1 \
|
||||
| sed -E 's/^[[:space:]]*public_url:[[:space:]]*//; s/[[:space:]]+#.*$//; s/[[:space:]]*$//' \
|
||||
|| true)"
|
||||
CFG_PUBLIC_URL="${CFG_PUBLIC_URL#[\'\"]}"; CFG_PUBLIC_URL="${CFG_PUBLIC_URL%[\'\"]}"
|
||||
CFG_PUBLIC_URL="${CFG_PUBLIC_URL%/}"
|
||||
echo " 1. In your Route 53 private hosted zone, alias the host of"
|
||||
echo " ${CFG_PUBLIC_URL:-<public_url>} to the ALB: ${ALB_DNS}"
|
||||
echo " (the ALB's own *.elb.amazonaws.com name can't carry your ACM certificate)."
|
||||
echo " 2. Register this redirect URI on the Okta OIDC web app: ${CFG_PUBLIC_URL:-<public_url>}/oauth/callback"
|
||||
echo " 3. Verify from inside your corporate network:"
|
||||
echo " curl -s ${CFG_PUBLIC_URL:-<public_url>}/.well-known/oauth-authorization-server"
|
||||
fi
|
||||
|
||||
# ---- summary ----------------------------------------------------------------
|
||||
cat <<EOF
|
||||
|
||||
==> Done.
|
||||
|
||||
Security groups ${ALB_SG_NAME}=${ALB_SG} ${GW_SG_NAME}=${GW_SG} ${DB_SG_NAME}=${DB_SG}
|
||||
IAM roles ${TASK_ROLE} (bedrock-invoke), ${EXEC_ROLE} (pull + secrets)
|
||||
Image ${IMAGE:-(not built yet — fill ${GATEWAY_YAML})}
|
||||
RDS instance ${DB_INSTANCE} -> ${DB_HOST}
|
||||
Database / user ${DB_NAME} / ${DB_USER}
|
||||
Secrets ${SECRET_NAME}, ${JWT_SECRET_NAME}, ${OIDC_SECRET_NAME}$( [[ -n "${OIDC_ARN}" ]] || printf ' (MISSING — create it)' )
|
||||
ECS service ${CLUSTER}/${SERVICE} behind ${ALB_DNS:-(not deployed yet)}
|
||||
|
||||
Next steps (see https://code.claude.com/docs/en/claude-apps-gateway-on-aws):
|
||||
- Create the one operator-provided secret (from the Okta OIDC web app). Put the
|
||||
client secret in a 0600 file first — passing it as a literal argument would
|
||||
leave it readable in the process table and in audit/EDR logs:
|
||||
aws secretsmanager create-secret --name ${OIDC_SECRET_NAME} \\
|
||||
--secret-string file:///path/to/okta-client-secret.txt
|
||||
- Fill in the REPLACE_ME values in ${GATEWAY_YAML}, then re-run: setup.sh builds the
|
||||
image (config baked in) and deploys once the secret and ACM_CERT_ARN exist.
|
||||
- Enable Bedrock model access in the console for the Claude models you need (per
|
||||
region the us.anthropic.* profiles span) and submit the one-time use case form.
|
||||
- Alias your internal hostname (gateway.yaml public_url) to the ALB in a Route 53
|
||||
private hosted zone, and register <public_url>/oauth/callback on the Okta app.
|
||||
- The gateway runs its own schema migrations at boot, so ${DB_USER} needs CREATE TABLE.
|
||||
EOF
|
||||
19
examples/gateway/aws/terraform/.gitignore
vendored
Normal file
19
examples/gateway/aws/terraform/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Never commit state (contains secrets) or local var files
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
.terraform/
|
||||
terraform.tfvars
|
||||
*.auto.tfvars
|
||||
crash.log
|
||||
|
||||
# The lock file holds no secrets. It's ignored here so consumers who copy this
|
||||
# example into their own repo generate (and commit) their own platform-complete
|
||||
# lock at first init — committing one from this repo would carry only one
|
||||
# platform's provider hashes. In your copy, drop this line and commit the lock
|
||||
# produced by:
|
||||
# terraform providers lock -platform=linux_amd64 -platform=linux_arm64 \
|
||||
# -platform=darwin_amd64 -platform=darwin_arm64 -platform=windows_amd64
|
||||
# versions.tf pins by range only, so without a committed lock the registry
|
||||
# serves the newest in-range build; a platform-complete lock gives hash
|
||||
# continuity across machines/CI and makes provider upgrades reviewable diffs.
|
||||
.terraform.lock.hcl
|
||||
182
examples/gateway/aws/terraform/README.md
Normal file
182
examples/gateway/aws/terraform/README.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Claude apps gateway — Terraform (ECS Fargate)
|
||||
|
||||
Terraform equivalent of `../setup.sh`. Lets end-users provision and manage
|
||||
the gateway with `terraform apply`. Covers the same scope ([walkthrough](https://code.claude.com/docs/en/claude-apps-gateway-on-aws) §1–7,
|
||||
ECS track): security groups → task + execution IAM roles → ECR repository →
|
||||
private-subnet RDS for PostgreSQL → Secrets Manager secrets → ECS Fargate
|
||||
service behind an internal ALB. The VPC and private subnets are walkthrough
|
||||
prerequisites, passed in as variables — unlike the GCP example, no network is
|
||||
created here.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `versions.tf` | Provider pins (aws, random) |
|
||||
| `variables.tf` | All inputs (defaults match `setup.sh`'s) |
|
||||
| `main.tf` | Resources |
|
||||
| `outputs.tf` | ALB DNS name + zone ID, image, roles, DB endpoint |
|
||||
| `terraform.tfvars.example` | Copy to `terraform.tfvars` and edit |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **`../gateway.yaml` created and FULLY filled in** — copy the template first:
|
||||
`cp ../gateway.yaml.example ../gateway.yaml`, then replace every `REPLACE_ME`
|
||||
(Terraform reads this file and enforces no `REPLACE_ME` via a precondition).
|
||||
Unlike the GCP example there is no placeholder-first-pass: the config is
|
||||
**baked into the image**, and `public_url` is your own internal hostname,
|
||||
which you choose up front (you already hold its ACM certificate).
|
||||
`gateway.yaml` is gitignored; the committed template is `gateway.yaml.example`.
|
||||
2. The **prebuilt linux-x64 `claude` binary at `../claude`** — the Claude Code
|
||||
release binary, which includes the `gateway` subcommand (see the
|
||||
[walkthrough](https://code.claude.com/docs/en/claude-apps-gateway-on-aws)).
|
||||
See `../setup.sh`'s `DIST_URL`/`DIST_SHA256` download path for a
|
||||
checksum-verified fetch.
|
||||
3. A **VPC with two+ private subnets** in different AZs and NAT egress, an **ACM
|
||||
certificate** for your internal gateway hostname, and **Bedrock model access**
|
||||
enabled in the console (cross-region `us.anthropic.*` profiles need it in each
|
||||
region the profile spans), with the one-time use case form submitted.
|
||||
4. A **remote backend** for shared use (see below). State holds secrets — never commit it.
|
||||
|
||||
## Deploy
|
||||
|
||||
Terraform creates the ECR repository but does **not** build/push the image, so
|
||||
the apply is two passes: a targeted apply to create the repo, then build/push,
|
||||
then the full apply.
|
||||
|
||||
```bash
|
||||
cp terraform.tfvars.example terraform.tfvars # edit it
|
||||
terraform init
|
||||
|
||||
# Pin providers in your copy (once, then commit .terraform.lock.hcl and drop
|
||||
# its .gitignore line): versions.tf pins by range only, so without a committed
|
||||
# lock the registry serves the newest in-range build — a platform-complete
|
||||
# lock gives hash continuity across machines/CI and makes provider upgrades
|
||||
# reviewable diffs.
|
||||
terraform providers lock -platform=linux_amd64 -platform=linux_arm64 \
|
||||
-platform=darwin_amd64 -platform=darwin_arm64 -platform=windows_amd64
|
||||
|
||||
# 1. Create just the ECR repository (the -target warning is expected):
|
||||
terraform apply -target=aws_ecr_repository.repo
|
||||
|
||||
# 2. Build and push the image (gateway.yaml and the RDS CA bundle are baked in;
|
||||
# the COPY sources are context-relative — the build context `..` is aws/, so
|
||||
# `claude`, `gateway.yaml`, and `rds-global-bundle.pem`).
|
||||
# The CA bundle is the trust anchor for the connection string's
|
||||
# sslmode=verify-full (AWS rotates it; download it when absent — don't commit it):
|
||||
curl -fL --proto '=https' -o ../rds-global-bundle.pem \
|
||||
https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
|
||||
aws ecr get-login-password --region us-east-1 \
|
||||
| docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com
|
||||
docker build --platform=linux/amd64 --provenance=false \
|
||||
-f ../Dockerfile --build-arg CLAUDE_BINARY=claude --build-arg GATEWAY_CONFIG=gateway.yaml \
|
||||
-t <account-id>.dkr.ecr.us-east-1.amazonaws.com/claude-gateway:<version> ..
|
||||
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/claude-gateway:<version>
|
||||
|
||||
# 3. Full apply:
|
||||
terraform apply
|
||||
```
|
||||
|
||||
Set in `terraform.tfvars`:
|
||||
|
||||
- `region`, `vpc_id`, `private_subnet_ids`, `corporate_cidr`
|
||||
- `acm_certificate_arn` — the certificate for your internal gateway hostname
|
||||
(`gateway.yaml`'s `public_url` host), served by the ALB's HTTPS listener
|
||||
- `image_tag` (after building/pushing — step 2 above). The repo enforces
|
||||
**immutable tags**, so a `gateway.yaml` edit means a rebuild under a **new**
|
||||
tag and an `image_tag` bump (`../setup.sh` automates this by tagging
|
||||
`<version>-cfg<sha8-of-gateway.yaml>`)
|
||||
- **`oidc_client_secret`** — required (the ECS tasks inject `latest` of this
|
||||
secret at start; with no version they fail with
|
||||
`ResourceInitializationError`). Terraform creates the secret + version from it.
|
||||
|
||||
## Tear down
|
||||
|
||||
Tear down a trial with `terraform destroy`: set `deletion_protection = false`,
|
||||
run `terraform apply` to record that on RDS and the ALB (and to flip RDS to
|
||||
`skip_final_snapshot` — the provider checks the value in **state**, not config,
|
||||
so destroy would still refuse otherwise), then `terraform destroy`.
|
||||
|
||||
The same switch drives the Secrets Manager recovery window: the three secrets
|
||||
have **fixed names**, and a secret deleted with the default 30-day recovery
|
||||
window keeps its name reserved — a later `terraform apply` would fail with a
|
||||
name conflict until the window elapses. With `deletion_protection = false` the
|
||||
destroy deletes them immediately (`recovery_window_in_days = 0`). If you
|
||||
destroyed a deployment that still had `deletion_protection = true` (or tore
|
||||
down an older copy of this module), clear the scheduled deletions before
|
||||
re-applying:
|
||||
|
||||
```bash
|
||||
for s in gateway-postgres-url gateway-jwt-secret gateway-oidc-client-secret; do
|
||||
aws secretsmanager delete-secret --secret-id "$s" --force-delete-without-recovery
|
||||
done
|
||||
```
|
||||
|
||||
## Guard rails
|
||||
|
||||
Tuned so accidental deletion is hard but greenfield teardown stays easy:
|
||||
|
||||
- `deletion_protection = true` (variable, default true) on RDS and the ALB —
|
||||
blocks accidental deletion; set `false` when you intend to `terraform destroy`.
|
||||
The same switch controls RDS `skip_final_snapshot`, so a protected instance
|
||||
always leaves a final snapshot.
|
||||
- ECR tags are **immutable** and **scanned on push** — a deployed tag can never
|
||||
be silently re-pointed at different bytes. For production, also restrict push
|
||||
rights on the repo to your CI / image-promotion pipeline rather than operator
|
||||
credentials.
|
||||
- The IAM roles carry only the walkthrough's least-privilege documents: Bedrock
|
||||
invoke on the Anthropic model ARNs (task role) and `secretsmanager:GetSecretValue`
|
||||
on exactly the three secrets this module creates (by ARN) plus the AWS-managed
|
||||
ECS execution policy (execution role). Inline policies are scoped to these
|
||||
roles, so nothing else in the account is touched.
|
||||
- TLS everywhere it terminates: the ALB listener pins
|
||||
`ELBSecurityPolicy-TLS13-1-2-2021-06` (no TLS 1.0/1.1), and the store
|
||||
connection uses `sslmode=verify-full` against the RDS CA bundle baked into
|
||||
the image, with `rds.force_ssl=1` enforcing TLS server-side.
|
||||
|
||||
## Private access
|
||||
|
||||
The ALB is **internal** with `ip_address_type = "ipv4"` (a dual-stack internal
|
||||
ALB publishes public-range AAAA records, which the CLI's `/login`
|
||||
private-network check rejects), and its security group admits only
|
||||
`corporate_cidr` on 443. Reaching it from on-prem requires your existing
|
||||
routing into the VPC (Direct Connect / VPN) — **operator / network-team-owned**
|
||||
plumbing this module does not create.
|
||||
|
||||
After the apply, give developers a privately resolvable hostname: in a Route 53
|
||||
private hosted zone, alias the host of `gateway.yaml`'s `public_url` to the ALB
|
||||
(`alb_dns_name` / `alb_zone_id` outputs). The ALB's own `*.elb.amazonaws.com`
|
||||
name can't carry your ACM certificate, so use your own name.
|
||||
|
||||
The tasks run in the private subnets with no public IP; all egress (Bedrock,
|
||||
the IdP, Secrets Manager, ECR, CloudWatch Logs) goes through the NAT gateway.
|
||||
To keep Bedrock traffic off the public path, create a `bedrock-runtime`
|
||||
interface VPC endpoint and point the upstream's `base_url` at it (see
|
||||
`../gateway.yaml.example`); the IdP still needs internet egress.
|
||||
|
||||
## Remote state (recommended for teams)
|
||||
|
||||
Add a backend so state is shared and locked (and out of git):
|
||||
|
||||
```hcl
|
||||
# backend.tf
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "<your-tf-state-bucket>"
|
||||
key = "claude-gateway/ecs"
|
||||
region = "us-east-1"
|
||||
use_lockfile = true # S3-native locking (Terraform >= 1.10); or set dynamodb_table
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## After deploy
|
||||
|
||||
- `terraform output alb_dns_name` / `alb_zone_id` — create the Route 53 alias.
|
||||
- Register `<public_url>/oauth/callback` on the Okta OIDC web app and make sure
|
||||
`../gateway.yaml` `public_url` matches the host you aliased.
|
||||
- Notes: Terraform does not build the image. To ship a new gateway version **or
|
||||
a config edit**, rerun the docker build/push under a new tag and bump
|
||||
`image_tag` — secrets-only rotations roll the service without a rebuild (the
|
||||
task definition stamps a hash of the managed secret values), but a
|
||||
`gateway.yaml` edit reaches the container only through the rebuilt image.
|
||||
510
examples/gateway/aws/terraform/main.tf
Normal file
510
examples/gateway/aws/terraform/main.tf
Normal file
@@ -0,0 +1,510 @@
|
||||
# Claude apps gateway on ECS Fargate — Terraform equivalent of setup.sh.
|
||||
# Section markers (§N) map to setup.sh and the walkthrough:
|
||||
# https://code.claude.com/docs/en/claude-apps-gateway-on-aws
|
||||
#
|
||||
# Unlike the GCP example this module does NOT create the network — the VPC and
|
||||
# private subnets are walkthrough prerequisites, passed in as variables.
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
data "aws_region" "current" {}
|
||||
|
||||
# Read (not created) so a typo'd VPC or subnet ID fails the plan up front
|
||||
# instead of half-applying.
|
||||
data "aws_vpc" "this" {
|
||||
id = var.vpc_id
|
||||
}
|
||||
|
||||
data "aws_subnet" "private" {
|
||||
for_each = toset(var.private_subnet_ids)
|
||||
id = each.value
|
||||
}
|
||||
|
||||
locals {
|
||||
config_path = var.gateway_config_path != "" ? var.gateway_config_path : "${path.module}/../gateway.yaml"
|
||||
gateway_config = file(local.config_path)
|
||||
image = "${aws_ecr_repository.repo.repository_url}:${var.image_tag}"
|
||||
}
|
||||
|
||||
# ── 1 Security groups ───────────────────────────────────────────────────────
|
||||
# Three groups chain the traffic path: corp network -> ALB :443, ALB ->
|
||||
# gateway :8080, gateway -> Postgres :5432. Nothing else is reachable.
|
||||
# Rules are separate resources (not inline) so they never fight other tooling.
|
||||
resource "aws_security_group" "alb" {
|
||||
name = "claude-gateway-alb"
|
||||
description = "Claude gateway ALB"
|
||||
vpc_id = var.vpc_id
|
||||
}
|
||||
|
||||
resource "aws_security_group" "gateway" {
|
||||
name = "claude-gateway-svc"
|
||||
description = "Claude gateway service"
|
||||
vpc_id = var.vpc_id
|
||||
}
|
||||
|
||||
resource "aws_security_group" "db" {
|
||||
name = "claude-gateway-db"
|
||||
description = "Claude gateway Postgres"
|
||||
vpc_id = var.vpc_id
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "alb_https" {
|
||||
security_group_id = aws_security_group.alb.id
|
||||
description = "HTTPS from the corporate network"
|
||||
ip_protocol = "tcp"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
cidr_ipv4 = var.corporate_cidr
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "gateway_from_alb" {
|
||||
security_group_id = aws_security_group.gateway.id
|
||||
description = "Gateway port from the ALB"
|
||||
ip_protocol = "tcp"
|
||||
from_port = 8080
|
||||
to_port = 8080
|
||||
referenced_security_group_id = aws_security_group.alb.id
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "db_from_gateway" {
|
||||
security_group_id = aws_security_group.db.id
|
||||
description = "Postgres from the gateway"
|
||||
ip_protocol = "tcp"
|
||||
from_port = 5432
|
||||
to_port = 5432
|
||||
referenced_security_group_id = aws_security_group.gateway.id
|
||||
}
|
||||
|
||||
# Egress: the ALB only needs to reach its targets; the gateway needs the NAT
|
||||
# path out (Bedrock, the IdP, Secrets Manager, ECR, CloudWatch Logs) plus
|
||||
# Postgres. The DB group needs no egress (security groups are stateful).
|
||||
resource "aws_vpc_security_group_egress_rule" "alb_to_gateway" {
|
||||
security_group_id = aws_security_group.alb.id
|
||||
description = "Health checks + forwarding to gateway tasks"
|
||||
ip_protocol = "tcp"
|
||||
from_port = 8080
|
||||
to_port = 8080
|
||||
referenced_security_group_id = aws_security_group.gateway.id
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_egress_rule" "gateway_all" {
|
||||
security_group_id = aws_security_group.gateway.id
|
||||
description = "Egress to Bedrock, the IdP, Secrets Manager, ECR, CloudWatch Logs, Postgres"
|
||||
ip_protocol = "-1"
|
||||
cidr_ipv4 = "0.0.0.0/0"
|
||||
}
|
||||
|
||||
# ── 2 IAM roles (least-privilege) ───────────────────────────────────────────
|
||||
# Task role: the gateway's runtime identity. Its ONLY permission is invoking
|
||||
# Claude models on Bedrock — the upstream's `auth: {}` resolves to this role
|
||||
# via the AWS default credential chain. The policy must cover both the
|
||||
# cross-region inference-profile ARNs and the underlying foundation-model ARNs.
|
||||
data "aws_iam_policy_document" "ecs_trust" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRole"]
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["ecs-tasks.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "task" {
|
||||
name = var.task_role_name
|
||||
assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "bedrock_invoke" {
|
||||
name = "bedrock-invoke"
|
||||
role = aws_iam_role.task.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
|
||||
Resource = [
|
||||
"arn:aws:bedrock:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:inference-profile/us.anthropic.*",
|
||||
"arn:aws:bedrock:*::foundation-model/anthropic.*",
|
||||
]
|
||||
}]
|
||||
})
|
||||
|
||||
# The walkthrough is scoped to commercial US regions: this policy and the
|
||||
# gateway's built-in model catalog both use the us.anthropic.* geo-prefixed
|
||||
# cross-region inference profiles, which only exist in the commercial US
|
||||
# regions — an explicit list, not a `us-` prefix match, because GovCloud
|
||||
# (us-gov-*) and ISO (us-iso-*) regions share the prefix but live in
|
||||
# different AWS partitions where those profiles and this module's arn:aws:
|
||||
# ARNs are wrong. Anywhere else the deploy provisions fine and then every
|
||||
# model call fails. Other-region deploys must pin region-appropriate
|
||||
# profiles via a models: block in gateway.yaml (see the config reference's
|
||||
# models: guidance: https://code.claude.com/docs/en/claude-apps-gateway-config),
|
||||
# widen the inference-profile ARN geo prefix above, and set
|
||||
# allow_non_us_region = true.
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = var.allow_non_us_region || contains(["us-east-1", "us-east-2", "us-west-1", "us-west-2"], var.region)
|
||||
error_message = "region is not a commercial US region (GovCloud/ISO share the us- prefix but are different partitions), and this module's IAM policy and the built-in model catalog use the US-geo (us.anthropic.*) inference profiles. Pin your region's inference profiles in a models: block in gateway.yaml, adjust the bedrock-invoke ARN prefix, then set allow_non_us_region = true."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Execution role: the ECS agent's identity — pulls the image from ECR and
|
||||
# injects the Secrets Manager values into the container; the gateway never
|
||||
# uses it. AmazonECSTaskExecutionRolePolicy covers the ECR pull + awslogs;
|
||||
# the inline policy adds read on exactly the three secrets this module
|
||||
# creates — their full ARNs, not a name-prefix wildcard, so nothing else
|
||||
# in a shared account (present or future) is readable through this role.
|
||||
resource "aws_iam_role" "execution" {
|
||||
name = var.execution_role_name
|
||||
assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "execution_managed" {
|
||||
role = aws_iam_role.execution.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "secrets_read" {
|
||||
name = "read-gateway-secrets"
|
||||
role = aws_iam_role.execution.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Action = "secretsmanager:GetSecretValue"
|
||||
Resource = [
|
||||
aws_secretsmanager_secret.jwt.arn,
|
||||
aws_secretsmanager_secret.oidc.arn,
|
||||
aws_secretsmanager_secret.postgres_url.arn,
|
||||
]
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
# ── 6 ECR repository ────────────────────────────────────────────────────────
|
||||
# NOTE: image build/push is a separate step (see README) — Terraform only makes
|
||||
# the repo. IMMUTABLE tags + scan-on-push: the ECS service pulls whatever this
|
||||
# repo serves under the deployed tag, so a pushed tag must never be silently
|
||||
# re-pointed. For production, also restrict push rights on this repo to your
|
||||
# CI / image-promotion pipeline rather than operator credentials.
|
||||
resource "aws_ecr_repository" "repo" {
|
||||
name = var.ecr_repo
|
||||
image_tag_mutability = "IMMUTABLE"
|
||||
image_scanning_configuration {
|
||||
scan_on_push = true
|
||||
}
|
||||
}
|
||||
|
||||
# ── 3 RDS for PostgreSQL (private subnets, no public address) ───────────────
|
||||
resource "aws_db_subnet_group" "db" {
|
||||
name = var.db_instance
|
||||
description = "Claude gateway"
|
||||
subnet_ids = var.private_subnet_ids
|
||||
}
|
||||
|
||||
# rds.force_ssl: reject plaintext connections server-side — the client-side
|
||||
# counterpart is sslmode=verify-full in the connection string (§5). The family
|
||||
# tracks the major version in var.db_engine_version.
|
||||
#
|
||||
# name_prefix + create_before_destroy: a major engine bump changes `family`,
|
||||
# which forces replacement — with a static name that deadlocks (the new group
|
||||
# can't be created under the taken name; the old can't be destroyed while the
|
||||
# live instance uses it: "parameter group is currently in use"). With this
|
||||
# shape the replacement group gets a fresh unique name, the instance is
|
||||
# repointed, then the old group is destroyed. (The subnet group above needs
|
||||
# neither: subnet_ids update in place and an engine bump never touches it.)
|
||||
resource "aws_db_parameter_group" "db" {
|
||||
name_prefix = "${var.db_instance}-"
|
||||
family = "postgres${split(".", var.db_engine_version)[0]}"
|
||||
description = "Claude gateway - require TLS on every connection"
|
||||
|
||||
parameter {
|
||||
name = "rds.force_ssl"
|
||||
value = "1"
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
# URL-safe (alphanumeric) so it drops cleanly into the connection string.
|
||||
# nosemgrep: terraform-generic-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "random_password" "db" {
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-aws-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "aws_db_instance" "db" {
|
||||
identifier = var.db_instance
|
||||
engine = "postgres"
|
||||
engine_version = var.db_engine_version
|
||||
instance_class = var.db_instance_class
|
||||
allocated_storage = var.db_allocated_storage
|
||||
db_name = var.db_name
|
||||
username = var.db_user
|
||||
password = random_password.db.result
|
||||
db_subnet_group_name = aws_db_subnet_group.db.name
|
||||
parameter_group_name = aws_db_parameter_group.db.name
|
||||
vpc_security_group_ids = [aws_security_group.db.id]
|
||||
publicly_accessible = false
|
||||
storage_encrypted = true
|
||||
deletion_protection = var.deletion_protection
|
||||
# Greenfield teardown: skip the final snapshot only once deletion protection
|
||||
# is deliberately turned off (the same switch — see README "Tear down").
|
||||
skip_final_snapshot = !var.deletion_protection
|
||||
final_snapshot_identifier = "${var.db_instance}-final"
|
||||
}
|
||||
|
||||
# ── 5 Secrets Manager ───────────────────────────────────────────────────────
|
||||
# postgres-url: connection string built from the instance's private endpoint.
|
||||
# The execution role's policy (§2) grants read on these three secrets' ARNs
|
||||
# and nothing else.
|
||||
#
|
||||
# recovery_window_in_days rides the same switch as skip_final_snapshot: the
|
||||
# secrets have fixed names, so a destroy that leaves them in the default
|
||||
# 30-day scheduled-deletion state makes the next apply fail with a name
|
||||
# conflict. Greenfield teardown (deletion_protection = false) deletes them
|
||||
# immediately; a protected deployment keeps the 30-day recovery window.
|
||||
resource "aws_secretsmanager_secret" "postgres_url" {
|
||||
name = var.secret_name
|
||||
recovery_window_in_days = var.deletion_protection ? 30 : 0
|
||||
}
|
||||
|
||||
# sslmode=verify-full: the gateway's driver (Bun.SQL) honors sslmode from the
|
||||
# URL and verifies the server certificate chain AND hostname. The trust anchor
|
||||
# is the AWS RDS CA bundle baked into the image at /etc/claude/rds-global-bundle.pem
|
||||
# and loaded via NODE_EXTRA_CA_CERTS (see ../Dockerfile) — do NOT add a
|
||||
# libpq-style `sslrootcert=` query param: the driver doesn't read it and
|
||||
# forwards it to Postgres as a startup parameter, which the server rejects.
|
||||
# nosemgrep: terraform-aws-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "aws_secretsmanager_secret_version" "postgres_url" {
|
||||
secret_id = aws_secretsmanager_secret.postgres_url.id
|
||||
secret_string = "postgres://${var.db_user}:${random_password.db.result}@${aws_db_instance.db.address}:5432/${var.db_name}?sslmode=verify-full"
|
||||
}
|
||||
|
||||
# jwt: session signing key.
|
||||
# nosemgrep: terraform-generic-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "random_password" "jwt" {
|
||||
length = 48
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret" "jwt" {
|
||||
name = var.jwt_secret_name
|
||||
recovery_window_in_days = var.deletion_protection ? 30 : 0 # see postgres_url
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-aws-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "aws_secretsmanager_secret_version" "jwt" {
|
||||
secret_id = aws_secretsmanager_secret.jwt.id
|
||||
secret_string = random_password.jwt.result
|
||||
}
|
||||
|
||||
# oidc client secret: operator-provided (from the Okta OIDC web app).
|
||||
resource "aws_secretsmanager_secret" "oidc" {
|
||||
name = var.oidc_secret_name
|
||||
recovery_window_in_days = var.deletion_protection ? 30 : 0 # see postgres_url
|
||||
}
|
||||
|
||||
# nosemgrep: terraform-aws-secrets-in-state -- secrets in tfstate are inherent to TF; mitigated by the documented remote S3 backend (see README "Remote state")
|
||||
resource "aws_secretsmanager_secret_version" "oidc" {
|
||||
count = var.oidc_client_secret != "" ? 1 : 0
|
||||
secret_id = aws_secretsmanager_secret.oidc.id
|
||||
secret_string = var.oidc_client_secret
|
||||
}
|
||||
|
||||
# Warn (not block) at plan time when the OIDC secret value isn't set: the task
|
||||
# definition references the secret unconditionally, so an empty value with no
|
||||
# out-of-band version means the tasks fail to start late, at container init
|
||||
# (ResourceInitializationError). A warning (not a precondition) keeps the
|
||||
# documented out-of-band-version mode usable.
|
||||
check "oidc_client_secret_set" {
|
||||
assert {
|
||||
condition = var.oidc_client_secret != ""
|
||||
error_message = "oidc_client_secret is empty — set it in terraform.tfvars, or add a version to the gateway-oidc-client-secret secret out-of-band before applying (the ECS tasks inject it at start and will fail without one)."
|
||||
}
|
||||
}
|
||||
|
||||
# ── 7 ECS Fargate service + internal ALB ────────────────────────────────────
|
||||
resource "aws_ecs_cluster" "cluster" {
|
||||
name = var.cluster_name
|
||||
}
|
||||
|
||||
# The gateway's stderr carries both its audit events and operational logs.
|
||||
# Bounded retention — without it the group keeps logs forever and cost grows
|
||||
# unbounded; the default (90 days) is sized for audit-trail review windows.
|
||||
resource "aws_cloudwatch_log_group" "gateway" {
|
||||
name = var.log_group_name
|
||||
retention_in_days = var.log_retention_days
|
||||
}
|
||||
|
||||
# Task definition. gateway.yaml ships INSIDE the image (unlike the GCP example,
|
||||
# which mounts it from Secret Manager), so Terraform reads ../gateway.yaml only
|
||||
# to (a) enforce the no-REPLACE_ME guard before a deploy and (b) stamp a hash
|
||||
# of the config + every managed secret value into the container environment —
|
||||
# secrets are injected at task start, so rotating one (tainting
|
||||
# random_password.db ALTERs the DB password; a new oidc_client_secret) would
|
||||
# otherwise leave running tasks on stale values with nothing forcing a roll.
|
||||
# NOTE the hash only forces a roll; a config EDIT still reaches the container
|
||||
# only via a rebuilt image — push under a new tag (the repo enforces
|
||||
# immutability) and bump image_tag, or the roll redeploys the old config.
|
||||
resource "aws_ecs_task_definition" "gateway" {
|
||||
family = var.service_name
|
||||
network_mode = "awsvpc"
|
||||
requires_compatibilities = ["FARGATE"]
|
||||
cpu = tostring(var.task_cpu)
|
||||
memory = tostring(var.task_memory)
|
||||
execution_role_arn = aws_iam_role.execution.arn
|
||||
task_role_arn = aws_iam_role.task.arn
|
||||
|
||||
runtime_platform {
|
||||
cpu_architecture = "X86_64" # build the image linux/amd64; ARM64 for Graviton (see ../Dockerfile)
|
||||
operating_system_family = "LINUX"
|
||||
}
|
||||
|
||||
container_definitions = jsonencode([
|
||||
{
|
||||
name = "gateway"
|
||||
image = local.image
|
||||
portMappings = [{ containerPort = 8080 }]
|
||||
environment = [
|
||||
{
|
||||
name = "GATEWAY_CONFIG_SHA"
|
||||
value = substr(sha256(join("", [
|
||||
local.gateway_config,
|
||||
random_password.db.result,
|
||||
random_password.jwt.result,
|
||||
var.oidc_client_secret,
|
||||
])), 0, 16)
|
||||
},
|
||||
]
|
||||
secrets = [
|
||||
{ name = "GATEWAY_JWT_SECRET", valueFrom = aws_secretsmanager_secret.jwt.arn },
|
||||
{ name = "OIDC_CLIENT_SECRET", valueFrom = aws_secretsmanager_secret.oidc.arn },
|
||||
{ name = "GATEWAY_POSTGRES_URL", valueFrom = aws_secretsmanager_secret.postgres_url.arn },
|
||||
]
|
||||
logConfiguration = {
|
||||
logDriver = "awslogs"
|
||||
options = {
|
||||
awslogs-group = aws_cloudwatch_log_group.gateway.name
|
||||
awslogs-region = data.aws_region.current.region
|
||||
awslogs-stream-prefix = "gateway"
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
# Guard mirrors setup.sh's REPLACE_ME check (non-comment lines): the config
|
||||
# is baked into the image this task definition deploys, so a half-filled
|
||||
# gateway.yaml at apply time means the pushed image is half-filled too.
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = length([
|
||||
for line in split("\n", local.gateway_config) :
|
||||
line
|
||||
if !startswith(trimspace(line), "#") && strcontains(line, "REPLACE_ME")
|
||||
]) == 0
|
||||
error_message = "gateway.yaml still has REPLACE_ME on a non-comment line — fill it in (and rebuild/push the image) before applying."
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
aws_secretsmanager_secret_version.postgres_url,
|
||||
aws_secretsmanager_secret_version.jwt,
|
||||
]
|
||||
}
|
||||
|
||||
# Internal ALB. ip_address_type ipv4: an internal dual-stack ALB publishes
|
||||
# public-range AAAA records, which the CLI's /login private-network check
|
||||
# rejects. idle_timeout 3600: the 60-second default closes a streaming
|
||||
# response at the first quiet period (long prompt processing before the first
|
||||
# token, extended thinking with no streamed output).
|
||||
resource "aws_lb" "gateway" {
|
||||
name = var.service_name
|
||||
internal = true
|
||||
load_balancer_type = "application"
|
||||
ip_address_type = "ipv4"
|
||||
subnets = var.private_subnet_ids
|
||||
security_groups = [aws_security_group.alb.id]
|
||||
idle_timeout = 3600
|
||||
enable_deletion_protection = var.deletion_protection
|
||||
}
|
||||
|
||||
# /readyz verifies the store is reachable, so a task that can't reach Postgres
|
||||
# never enters rotation (the gateway also serves liveness-only /healthz — see
|
||||
# the deploy guide's outage-behavior tradeoff).
|
||||
resource "aws_lb_target_group" "gateway" {
|
||||
name = var.service_name
|
||||
protocol = "HTTP"
|
||||
port = 8080
|
||||
vpc_id = var.vpc_id
|
||||
target_type = "ip"
|
||||
|
||||
health_check {
|
||||
path = "/readyz"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_lb_listener" "https" {
|
||||
load_balancer_arn = aws_lb.gateway.arn
|
||||
protocol = "HTTPS"
|
||||
port = 443
|
||||
# Explicit modern policy — omitting ssl_policy falls back to the legacy
|
||||
# ELBSecurityPolicy-2016-08 default, which still accepts TLS 1.0/1.1.
|
||||
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
|
||||
certificate_arn = var.acm_certificate_arn
|
||||
|
||||
default_action {
|
||||
type = "forward"
|
||||
target_group_arn = aws_lb_target_group.gateway.arn
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_ecs_service" "gateway" {
|
||||
name = var.service_name
|
||||
cluster = aws_ecs_cluster.cluster.id
|
||||
task_definition = aws_ecs_task_definition.gateway.arn
|
||||
desired_count = var.desired_count
|
||||
launch_type = "FARGATE"
|
||||
|
||||
# Stop a rollout whose tasks keep failing (bad image, unbootable config) and
|
||||
# roll back to the last steady state instead of relaunching failing tasks
|
||||
# forever.
|
||||
deployment_circuit_breaker {
|
||||
enable = true
|
||||
rollback = true
|
||||
}
|
||||
|
||||
network_configuration {
|
||||
subnets = var.private_subnet_ids
|
||||
security_groups = [aws_security_group.gateway.id]
|
||||
# All egress (Bedrock, the IdP, Secrets Manager, ECR, CloudWatch Logs)
|
||||
# goes through the NAT gateway — tasks get no public IP.
|
||||
assign_public_ip = false
|
||||
}
|
||||
|
||||
load_balancer {
|
||||
target_group_arn = aws_lb_target_group.gateway.arn
|
||||
container_name = "gateway"
|
||||
container_port = 8080
|
||||
}
|
||||
|
||||
# Tasks register with the ALB at start — give a cold task (image pull +
|
||||
# store connect + first /readyz) time before ECS replaces it as unhealthy.
|
||||
health_check_grace_period_seconds = 60
|
||||
|
||||
# The listener must exist before targets register; the secrets must be
|
||||
# readable before the first task starts.
|
||||
depends_on = [
|
||||
aws_lb_listener.https,
|
||||
aws_iam_role_policy.secrets_read,
|
||||
aws_iam_role_policy_attachment.execution_managed,
|
||||
aws_secretsmanager_secret_version.postgres_url,
|
||||
aws_secretsmanager_secret_version.jwt,
|
||||
aws_secretsmanager_secret_version.oidc,
|
||||
aws_db_instance.db,
|
||||
]
|
||||
}
|
||||
34
examples/gateway/aws/terraform/outputs.tf
Normal file
34
examples/gateway/aws/terraform/outputs.tf
Normal file
@@ -0,0 +1,34 @@
|
||||
output "alb_dns_name" {
|
||||
description = "Internal ALB DNS name. Alias your gateway hostname (the host in gateway.yaml's public_url) to this in a Route 53 private hosted zone — the *.elb.amazonaws.com name itself can't carry your ACM certificate."
|
||||
value = aws_lb.gateway.dns_name
|
||||
}
|
||||
|
||||
output "alb_zone_id" {
|
||||
description = "ALB hosted zone ID, for the Route 53 alias record."
|
||||
value = aws_lb.gateway.zone_id
|
||||
}
|
||||
|
||||
output "image" {
|
||||
description = "Image the service runs (build/push this separately — see README)."
|
||||
value = local.image
|
||||
}
|
||||
|
||||
output "ecr_repository_url" {
|
||||
description = "ECR repository URL to push the gateway image to."
|
||||
value = aws_ecr_repository.repo.repository_url
|
||||
}
|
||||
|
||||
output "task_role_arn" {
|
||||
description = "Gateway runtime task role (Bedrock invoke)."
|
||||
value = aws_iam_role.task.arn
|
||||
}
|
||||
|
||||
output "execution_role_arn" {
|
||||
description = "ECS execution role (image pull + secret injection)."
|
||||
value = aws_iam_role.execution.arn
|
||||
}
|
||||
|
||||
output "db_endpoint" {
|
||||
description = "RDS private endpoint (host only; the connection string lives in the gateway-postgres-url secret)."
|
||||
value = aws_db_instance.db.address
|
||||
}
|
||||
27
examples/gateway/aws/terraform/terraform.tfvars.example
Normal file
27
examples/gateway/aws/terraform/terraform.tfvars.example
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copy to terraform.tfvars and edit. terraform.tfvars is gitignored (see .gitignore).
|
||||
|
||||
region = "us-east-1" # a region where Bedrock serves the Claude models you need
|
||||
|
||||
# Prerequisite networking (NOT created by this module): the VPC and two+ private
|
||||
# subnets in different AZs with outbound internet via a NAT gateway.
|
||||
vpc_id = "vpc-..."
|
||||
private_subnet_ids = ["subnet-...a", "subnet-...b"]
|
||||
|
||||
# The only source the ALB admits on 443. Must not overlap the private subnets
|
||||
# above — hosts in the ALB subnets are trusted_proxies (gateway.yaml) and could
|
||||
# spoof client IPs via X-Forwarded-For.
|
||||
corporate_cidr = "10.0.0.0/8"
|
||||
|
||||
# ACM certificate for your internal gateway hostname (the host in gateway.yaml's
|
||||
# public_url), imported or issued by AWS Private CA.
|
||||
acm_certificate_arn = "arn:aws:acm:..."
|
||||
|
||||
image_tag = "<version>" # REQUIRED — the tag you build and push as linux/amd64 with
|
||||
# gateway.yaml baked in (setup.sh tags <version>-cfg<sha8>;
|
||||
# see README Deploy)
|
||||
|
||||
# Okta OIDC client secret: REQUIRED — uncomment and set it (Terraform creates the
|
||||
# secret version; the ECS tasks inject `gateway-oidc-client-secret` at start, so
|
||||
# without a version they fail with ResourceInitializationError). Leave empty only
|
||||
# if you add the secret version out-of-band.
|
||||
# oidc_client_secret = "..."
|
||||
189
examples/gateway/aws/terraform/variables.tf
Normal file
189
examples/gateway/aws/terraform/variables.tf
Normal file
@@ -0,0 +1,189 @@
|
||||
# Inputs — mirror the env-overridable knobs in setup.sh (same defaults).
|
||||
|
||||
variable "region" {
|
||||
description = "AWS region for everything this module creates. Pick one where Bedrock serves the Claude models you need. (The Bedrock region the gateway calls is set separately inside gateway.yaml — keep the two equal.) The walkthrough is scoped to the commercial US regions (us-east-1/us-east-2/us-west-1/us-west-2 — GovCloud and ISO regions are different partitions); see allow_non_us_region."
|
||||
type = string
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
variable "allow_non_us_region" {
|
||||
description = "The bedrock-invoke IAM policy and the gateway's built-in model catalog use the US-geo (us.anthropic.*) cross-region inference profiles, so any region outside the commercial US four (including GovCloud/ISO, which are different partitions) fails a plan-time precondition. Set true ONLY after pinning region-appropriate inference profiles via a models: block in gateway.yaml (see the config reference) and widening the ARN geo prefix in main.tf's bedrock-invoke policy."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
# ── Networking inputs (prerequisites — NOT created here) ────────────────────
|
||||
variable "vpc_id" {
|
||||
description = "Existing VPC ID (the walkthrough's prerequisite VPC). Unlike the GCP example, this module does not create the network."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "private_subnet_ids" {
|
||||
description = "Two+ private subnet IDs in different AZs, with outbound internet via a NAT gateway. The internal ALB, the ECS tasks, and the RDS subnet group all attach here."
|
||||
type = list(string)
|
||||
validation {
|
||||
condition = length(var.private_subnet_ids) >= 2
|
||||
error_message = "private_subnet_ids needs at least two subnets in different AZs (the internal ALB requires two)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "corporate_cidr" {
|
||||
description = "Your corporate network CIDR — the only source the ALB security group admits on 443. Must not overlap private_subnet_ids: hosts there are trusted_proxies (gateway.yaml) and could spoof client IPs via X-Forwarded-For."
|
||||
type = string
|
||||
}
|
||||
|
||||
# ── IAM (§2) ────────────────────────────────────────────────────────────────
|
||||
variable "task_role_name" {
|
||||
description = "ECS task role name (the gateway's runtime identity; its only permission is Bedrock invoke)."
|
||||
type = string
|
||||
default = "claude-gateway-task"
|
||||
}
|
||||
|
||||
variable "execution_role_name" {
|
||||
description = "ECS execution role name (the ECS agent's identity: pulls the image, injects the secrets)."
|
||||
type = string
|
||||
default = "claude-gateway-execution"
|
||||
}
|
||||
|
||||
# ── Image (§6) ──────────────────────────────────────────────────────────────
|
||||
# Terraform creates the ECR repository but does NOT build/push the image (that's
|
||||
# a docker build step — see README). It references the image by tag.
|
||||
variable "ecr_repo" {
|
||||
description = "ECR repository name."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Image tag — the tag you built and pushed (must already exist in the repo as linux/amd64, with gateway.yaml baked in). setup.sh tags as <version>-cfg<sha8 of gateway.yaml>; see the README Deploy section for the build command."
|
||||
type = string
|
||||
validation {
|
||||
condition = can(regex("^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$", var.image_tag))
|
||||
error_message = "image_tag must be a valid OCI tag — set it to the tag you pushed (the '<version>' in terraform.tfvars.example is a placeholder)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "gateway_config_path" {
|
||||
description = "Path to gateway.yaml. Empty = ../gateway.yaml relative to this module. Read for the REPLACE_ME guard and the config-sha that rolls the service; the file itself ships inside the image."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ── RDS (§3) ────────────────────────────────────────────────────────────────
|
||||
variable "db_instance" {
|
||||
description = "RDS instance identifier."
|
||||
type = string
|
||||
default = "claude-gateway-db"
|
||||
}
|
||||
|
||||
variable "db_engine_version" {
|
||||
description = "Postgres major version. The gateway supports PostgreSQL 14 or newer; 16 is the recommended default."
|
||||
type = string
|
||||
default = "16"
|
||||
}
|
||||
|
||||
variable "db_instance_class" {
|
||||
description = "RDS instance class."
|
||||
type = string
|
||||
default = "db.t4g.micro"
|
||||
}
|
||||
|
||||
variable "db_allocated_storage" {
|
||||
description = "RDS allocated storage in GiB."
|
||||
type = number
|
||||
default = 20
|
||||
}
|
||||
|
||||
variable "db_name" {
|
||||
description = "Database name."
|
||||
type = string
|
||||
default = "claude_gateway"
|
||||
}
|
||||
|
||||
variable "db_user" {
|
||||
description = "Database master user (the gateway connects as this role)."
|
||||
type = string
|
||||
default = "gateway"
|
||||
}
|
||||
|
||||
# ── Secrets (§5) ────────────────────────────────────────────────────────────
|
||||
# The execution role's secrets-read policy grants read on exactly these three
|
||||
# secrets' ARNs, so renames are picked up automatically on the next apply.
|
||||
variable "secret_name" {
|
||||
description = "Secrets Manager secret holding the Postgres connection string."
|
||||
type = string
|
||||
default = "gateway-postgres-url"
|
||||
}
|
||||
|
||||
variable "jwt_secret_name" {
|
||||
description = "Secrets Manager secret holding the session JWT signing key."
|
||||
type = string
|
||||
default = "gateway-jwt-secret"
|
||||
}
|
||||
|
||||
variable "oidc_secret_name" {
|
||||
description = "Secrets Manager secret holding the Okta OIDC client secret."
|
||||
type = string
|
||||
default = "gateway-oidc-client-secret"
|
||||
}
|
||||
|
||||
variable "oidc_client_secret" {
|
||||
description = "Okta OIDC client secret value. Leave empty to NOT manage the version via Terraform (only if you add the secret version out-of-band — without one the tasks fail to start)."
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# ── ECS + ALB (§7) ──────────────────────────────────────────────────────────
|
||||
variable "cluster_name" {
|
||||
description = "ECS cluster name."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
variable "service_name" {
|
||||
description = "ECS service name (also used for the ALB and target group)."
|
||||
type = string
|
||||
default = "claude-gateway"
|
||||
}
|
||||
|
||||
variable "log_group_name" {
|
||||
description = "CloudWatch Logs group for the gateway's stderr (audit events + operational logs)."
|
||||
type = string
|
||||
default = "/ecs/claude-gateway"
|
||||
}
|
||||
|
||||
variable "log_retention_days" {
|
||||
description = "CloudWatch Logs retention in days. The group carries the gateway's audit events, so align with your audit retention policy."
|
||||
type = number
|
||||
default = 90
|
||||
}
|
||||
|
||||
variable "acm_certificate_arn" {
|
||||
description = "ACM certificate ARN for the internal gateway hostname (the host in gateway.yaml's public_url), served by the ALB's HTTPS listener."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "task_cpu" {
|
||||
description = "Fargate task CPU units."
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
variable "task_memory" {
|
||||
description = "Fargate task memory (MiB)."
|
||||
type = number
|
||||
default = 2048
|
||||
}
|
||||
|
||||
variable "desired_count" {
|
||||
description = "ECS service desired task count. Each task opens a Postgres pool of up to 5 connections (the gateway's store.max_connections default) and db.t4g.micro caps at ~80 max_connections — keep desired_count × 5 below the DB class's limit, or raise the class before raising this."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "deletion_protection" {
|
||||
description = "Deletion protection on RDS and the ALB (and whether RDS skips the final snapshot on destroy). Keep true to avoid accidental deletion of the running deployment."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
18
examples/gateway/aws/terraform/versions.tf
Normal file
18
examples/gateway/aws/terraform/versions.tf
Normal file
@@ -0,0 +1,18 @@
|
||||
# Provider + version pins for the Claude apps gateway ECS Fargate deployment.
|
||||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 6.0, < 7.0" # 6.0 renames data.aws_region's attribute to `region`
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = ">= 3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
}
|
||||
442
feed.xml
442
feed.xml
@@ -6,7 +6,275 @@
|
||||
<author><name>Anthropic</name></author>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md"/>
|
||||
<link rel="self" type="application/atom+xml" href="https://raw.githubusercontent.com/anthropics/claude-code/main/feed.xml"/>
|
||||
<updated>2026-07-14T06:36:21Z</updated>
|
||||
<updated>2026-07-21T21:35:04Z</updated>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.217</id>
|
||||
<title>Claude Code v2.1.217</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.217"/>
|
||||
<updated>2026-07-21T21:35:04Z</updated>
|
||||
<content type="html"><p>• Added emoji shortcode autocomplete in the prompt input: type :heart: to insert ❤️, or :hea for suggestions — disable with the emojiCompletionEnabled setting</p>
|
||||
<p>• Added warnings when transcript writes are failing (e.g. disk full) or when session saving is off due to an inherited environment variable, instead of losing transcripts silently</p>
|
||||
<p>• Fixed a memory leak where truncated MCP tool outputs kept the full untruncated result in memory for the rest of the session</p>
|
||||
<p>• Fixed Windows auto-update failures that could leave claude.exe missing; failed updates now restore the preserved executable automatically</p>
|
||||
<p>• Fixed background session isolation not canonicalizing symlinked working directories, which could let sessions escape their workspace folder</p>
|
||||
<p>• Fixed auto-compact never triggering for Claude Opus 4.8 on Bedrock and /compact failing once over the limit</p>
|
||||
<p>• Fixed corporate mTLS, TLS-verify, OAuth scope, and proxy settings being ignored in Claude Desktop sessions</p>
|
||||
<p>• Fixed screen reader mode's startup announcement being cut off by the first prompt render, and the thinking status row re-rendering every few seconds to update elapsed time and token counts</p>
|
||||
<p>• Fixed managed settings that set OTEL_EXPORTER_OTLP_ENDPOINT not governing all signals — lower-scope signal-specific overrides no longer redirect telemetry away from the managed endpoint</p>
|
||||
<p>• Fixed --resume/--continue and /resume failing with a TypeError when a transcript has a malformed attachment entry</p>
|
||||
<p>• Fixed Remote Control sessions not showing a pending permission prompt or dialog to viewers that connected after it appeared</p>
|
||||
<p>• Fixed background shells sometimes becoming impossible to stop after a session is sent to the background (/background or ←) or when the session exits on a heavily loaded machine, most visible on Windows</p>
|
||||
<p>• Fixed a CLAUDE.md or SKILL.md paths frontmatter value with many brace groups OOM-killing or stalling the CLI at startup — brace expansion is now budget-bounded</p>
|
||||
<p>• Fixed the transcript preview sitting flush against the input area when attaching to a starting background session; it now leaves the same one-line gap as the live layout, so the transcript no longer shifts when the session takes over</p>
|
||||
<p>• Improved footer PR badge links to be clickable hyperlinks even when terminal support can't be detected (e.g. over ssh/tmux); set FORCE_HYPERLINK=0 to opt out</p>
|
||||
<p>• Changed the login-expiry warning to appear 3 days before expiry instead of 5</p>
|
||||
<p>• Capped the frontend-design plugin suggestion tip at 3 lifetime impressions instead of repeating indefinitely</p>
|
||||
<p>• Added a cap on concurrently-running subagents (default 20, override with CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS) so one message can't fan out unbounded background agents</p>
|
||||
<p>• Changed subagents to no longer spawn nested subagents by default; set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH to allow deeper nesting</p>
|
||||
<p>• Fixed --max-budget-usd not stopping background subagents: once the cap is reached, new spawns are denied and running background agents are halted</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.216</id>
|
||||
<title>Claude Code v2.1.216</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.216"/>
|
||||
<updated>2026-07-20T22:13:53Z</updated>
|
||||
<content type="html"><p>• Added sandbox.filesystem.disabled setting to skip filesystem isolation while keeping network egress control</p>
|
||||
<p>• Fixed a slowdown in long sessions where message normalization cost grew quadratically with the number of turns, causing multi-second stalls and slow resumes</p>
|
||||
<p>• Fixed auto mode denying commands with "HTTP 401" classifier errors after the OAuth token expired or rotated mid-session</p>
|
||||
<p>• Fixed AskUserQuestion telling Claude to continue even when your answer asked it to wait or explain first — free-text answers now get neutral wording</p>
|
||||
<p>• Fixed Claude Code on the web re-asking the same question and dropping your answer after the session sat idle for a few minutes</p>
|
||||
<p>• Fixed @-mentions silently attaching nothing after file-modifying hooks, vim dot-repeat of c-operators and paste, statusline running twice on resume, and resume-picker hangs on failure</p>
|
||||
<p>• Fixed resumed background agent sessions reverting to the default agent: the agent's prompt and tool restrictions are now restored</p>
|
||||
<p>• Fixed worktree-isolated subagents redirecting git into the shared checkout via git -C, --git-dir, or GIT_DIR/GIT_WORK_TREE</p>
|
||||
<p>• Fixed worktree sessions landing in another project's leftover worktree when the working directory did not match the selected project</p>
|
||||
<p>• Fixed background sessions whose worktree has no git repository being undeletable</p>
|
||||
<p>• Fixed claude daemon stop --any potentially terminating an unrelated process via a stale legacy daemon lockfile</p>
|
||||
<p>• Fixed Esc-Esc at an idle prompt not opening the rewind picker in long-running sessions with background tasks</p>
|
||||
<p>• Fixed Bash command permission checking for compound statements with redirects inside &amp;&amp; lists or negations</p>
|
||||
<p>• Fixed pressing Ctrl+X twice in the agent list failing to delete a session, and deleted sessions reappearing when their background worker had died</p>
|
||||
<p>• Fixed background subagents getting cancelled when a high-priority message arrives during their startup window</p>
|
||||
<p>• Fixed mouse and focus garbage in the terminal while a GUI editor from /memory, /plan, /keybindings, or Ctrl+G is open; /memory no longer waits for the editor to close</p>
|
||||
<p>• Fixed Claude-in-Chrome 403-looping on reconnect when the session's OAuth token lacks a required scope</p>
|
||||
<p>• Fixed workflow saves and scheduled-task writes following a symlink at .claude, which could redirect writes outside the project</p>
|
||||
<p>• Fixed MCP re-authenticate revoking working credentials before the new sign-in succeeds, and the reconnect needs-auth message in background sessions pointing at an unusable command</p>
|
||||
<p>• Fixed read-only commands on Windows accessing network paths without a permission prompt</p>
|
||||
<p>• Fixed Bash command parsing of non-ASCII characters to match real shell word boundaries</p>
|
||||
<p>• Fixed PowerShell tool permission validation of commands containing invisible Unicode characters</p>
|
||||
<p>• Fixed dialogs in fullscreen mode stretching past the right-hand edge of their panel</p>
|
||||
<p>• Fixed the /config settings list in fullscreen mode clipping its keyboard-hint footer</p>
|
||||
<p>• Fixed the transcript-mode (Ctrl+O) footer hint wrapping on terminals narrower than 104 columns</p>
|
||||
<p>• Fixed the Prometheus metrics endpoint (OTEL_METRICS_EXPORTER=prometheus) emitting invalid # UNIT lines</p>
|
||||
<p>• Fixed skills and commands changed during a session not appearing in the slash menu until restart</p>
|
||||
<p>• Fixed plugin skills with a name frontmatter field losing their plugin prefix in slash-command autocomplete</p>
|
||||
<p>• Fixed telemetry misreporting permission denials: failed permission-prompt requests no longer count as user rejections, and user interrupts are now reported as user aborts instead of rejections</p>
|
||||
<p>• Improved the /fork confirmation to one line with the new session's name, claude attach id, and a note when the copy shares your checkout</p>
|
||||
<p>• Improved validation of git and gh command arguments in the PowerShell tool</p>
|
||||
<p>• Improved the /ultrareview diff-too-large error to show configured limits, measured diff size, and largest contributing files</p>
|
||||
<p>• Improved /code-review ultra empty-diff message to name the exact base ref and suggest passing an explicit base</p>
|
||||
<p>• Improved the spend limit adjustment prompt to show the server's reason when a spend limit change is rejected</p>
|
||||
<p>• /context now shows an explicit warning when the conversation exceeds the context window, and a failed /compact displays as an error</p>
|
||||
<p>• /rewind no longer restores or deletes files through symlinks or hard links at tracked paths and reports how many paths it skipped</p>
|
||||
<p>• Background sessions: /mcp and /install-github-app now park a "needs input" request in the agent view when no client is attached</p>
|
||||
<p>• Updated the bundled dataviz skill: reordered the default chart palette and fixed guidance that suggested direct labels for four-series charts</p>
|
||||
<p>• [VSCode] Fixed right-to-left text (Arabic, Hebrew, Persian) rendering in the wrong order when mixed with English or code</p>
|
||||
<p>• Fixed cloud sessions dropping the in-flight message when the session's container restarts mid-turn — the interrupted turn now re-runs on resume instead of leaving the session unresponsive</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.215</id>
|
||||
<title>Claude Code v2.1.215</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.215"/>
|
||||
<updated>2026-07-19T02:55:54Z</updated>
|
||||
<content type="html"><p>• Claude no longer runs the /verify and /code-review skills on its own; invoke them with /verify or /code-review when you want them</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.214</id>
|
||||
<title>Claude Code v2.1.214</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.214"/>
|
||||
<updated>2026-07-18T01:20:23Z</updated>
|
||||
<content type="html"><p>• Fixed single-segment dir/ allow rules like Edit(src/) auto-approving writes to nested dir/ directories anywhere in the tree instead of only &lt;cwd&gt;/dir</p>
|
||||
<p>• Fixed a permission-check bypass affecting commands run in Windows PowerShell 5.1 sessions</p>
|
||||
<p>• Fixed Bash permission checks to fail closed on file-descriptor redirect forms that bash parses differently than the permission analyzer</p>
|
||||
<p>• Fixed Bash permission checks misjudging very long commands — commands over 10,000 characters now always prompt instead of running automatically</p>
|
||||
<p>• Fixed Bash permission checks treating zsh variable subscripts and modifiers in [[ ]] comparisons as inert text — these commands now prompt for approval</p>
|
||||
<p>• Fixed Bash permission checks to no longer auto-approve certain help and man commands that could run unsafe options, command substitutions, or backslash paths</p>
|
||||
<p>• Fixed permission prompts on remote sessions that could proceed before the local confirmation dialog</p>
|
||||
<p>• Added the EndConversation tool: Claude can end sessions with highly abusive users or jailbreak attempts, as on claude.ai since 2025 — see https://www.anthropic.com/research/end-subset-conversations</p>
|
||||
<p>• Added a periodic progress heartbeat for long-running tool calls that previously went silent</p>
|
||||
<p>• Added an ISO modified timestamp to memory file frontmatter</p>
|
||||
<p>• Added message.uuid, client_request_id, and tool_source attributes to OpenTelemetry log events for message-level correlation and tool provenance</p>
|
||||
<p>• Added CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH to configure the 60 KB truncation limit on OpenTelemetry content attributes</p>
|
||||
<p>• Added reasoning effort to the subagentStatusLine payload, so custom agent rows can render model and effort</p>
|
||||
<p>• Added permission prompts for docker commands (including the Podman docker shim) carrying daemon-redirect flags (--url, --connection, --identity, and Podman's remote mode) that previously ran without one</p>
|
||||
<p>• Fixed a crash when a GrowthBook feature evaluates to null, and a bug where a malformed flag payload could wipe the cached feature flags</p>
|
||||
<p>• Fixed Bash tool killing the Claude session when a pkill -f pattern accidentally matched the CLI's own process (Linux)</p>
|
||||
<p>• Fixed unbounded memory growth when --settings points at a device file or multi-GB file; oversized (&gt;2 MiB) settings files now fail at startup with a clear error</p>
|
||||
<p>• Fixed streaming turns failing with "Socket is closed" behind corporate proxies on Windows</p>
|
||||
<p>• Fixed stream-json output truncation at exit for slow-reading SDK/pipeline consumers; the exit drain now scales with queued bytes instead of a flat 2s cap</p>
|
||||
<p>• Fixed scheduled tasks refusing their own configured prompt as untrusted input — the fired prompt is now delivered as the session's assigned task</p>
|
||||
<p>• Fixed PowerShell tool commands hanging until timeout when a child process waited on standard input (Windows)</p>
|
||||
<p>• Fixed Python scripts under the PowerShell tool crashing with UnicodeDecodeError when reading non-UTF-8 data from standard input (Windows)</p>
|
||||
<p>• Fixed Python scripts run via the PowerShell tool crashing with UnicodeEncodeError on non-ASCII output, and PowerShell 7 error messages containing raw ANSI escape sequences (Windows)</p>
|
||||
<p>• Fixed the PowerShell tool reporting where.exe, fc.exe, and diff.exe as errors when they return a valid negative answer (Windows)</p>
|
||||
<p>• Fixed &gt; and &gt;&gt; under the PowerShell tool on Windows PowerShell 5.1 writing UTF-16LE files that other tools couldn't read as UTF-8</p>
|
||||
<p>• Fixed a displaced background daemon deleting its successor's control socket on shutdown, which made the next client kill the healthy replacement daemon</p>
|
||||
<p>• Fixed background sessions parked with ← or /background and left idle keeping the background daemon and a worker process alive indefinitely</p>
|
||||
<p>• Fixed completed background sessions being impossible to remove via claude rm or the agent view once the background service had gone idle</p>
|
||||
<p>• Fixed background sessions dispatched from a non-git folder being impossible to delete from the agents view</p>
|
||||
<p>• Fixed reopening a stopped background session failing to restore its saved conversation when an unreadable folder exists in the session store</p>
|
||||
<p>• Fixed the Remote Control "session ready" push notification firing for sessions where Remote Control was not explicitly enabled</p>
|
||||
<p>• Fixed /install-github-app and the /mcp settings menu being blocked in agent-view sessions — they're now refused only in background sessions with no terminal attached</p>
|
||||
<p>• Fixed plugins enabled via the --settings CLI flag not loading (regression since v2.1.181)</p>
|
||||
<p>• Fixed feature flags going stale in long-running sessions after the OAuth token rotates</p>
|
||||
<p>• Fixed /ultrareview refusing to run in repos with no merge base — it now offers to review all tracked files</p>
|
||||
<p>• Fixed claude update and claude doctor hanging silently, and the /status System diagnostics section going blank, when a shell-config path is a directory</p>
|
||||
<p>• Fixed memory frontmatter values being silently truncated at an inline # when memory files are saved</p>
|
||||
<p>• Fixed session cost and token telemetry double-counting on streams that emit multiple cumulative message_delta frames</p>
|
||||
<p>• Fixed a spurious "check your network" warning that appeared while the advisor was thinking</p>
|
||||
<p>• Fixed hooks with exit code 2 not blocking as documented when the hook's stdout JSON fails schema validation</p>
|
||||
<p>• Fixed OTel log events emitted outside the turn's async context missing the interaction span's trace context</p>
|
||||
<p>• Fixed MCP transient errors during prompts/resources refresh clearing the server's slash commands and resources</p>
|
||||
<p>• Improved the claude rc workspace-trust error in the home directory to say trust there is never saved and to suggest running from a project directory</p>
|
||||
<p>• Changed single-segment dir/ hook if: conditions to match only &lt;cwd&gt;/dir; write /dir/** for any-depth matching. deny/ask permission rules keep their any-depth match.</p>
|
||||
<p>• Changed file commands using -m/--magic-file or -f/--files-from to require permission instead of being auto-allowed as read-only</p>
|
||||
<p>• Changed keep-alive connection pooling to disable after a stale-connection error, so retries open a fresh socket</p>
|
||||
<p>• Changed SessionStart hooks to report source "fork" when a session begins as a fork instead of "resume"</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.212</id>
|
||||
<title>Claude Code v2.1.212</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.212"/>
|
||||
<updated>2026-07-17T00:26:21Z</updated>
|
||||
<content type="html"><p>• /fork now copies your conversation into a new background session (its own row in claude agents) while you keep working; the in-session subagent it used to launch is now /subtask</p>
|
||||
<p>• Added claude auto-mode reset to restore the default auto-mode configuration, with a confirmation prompt (pass --yes to skip)</p>
|
||||
<p>• Added a session-wide limit on WebSearch tool calls (default 200, tunable via CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION) to stop runaway search loops</p>
|
||||
<p>• Added a per-session cap on subagent spawns (default 200, override with CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION) to stop runaway delegation loops; /clear resets the budget</p>
|
||||
<p>• MCP tool calls running longer than 2 minutes now move to the background automatically so the session stays usable; configure the threshold or disable with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS</p>
|
||||
<p>• Typing /resume in the agent view now opens a picker of past sessions — including sessions deleted from the list — and resumes your pick as a background session</p>
|
||||
<p>• Fixed plan mode auto-running file-modifying Bash commands (e.g. touch, rm) without a permission prompt or SDK canUseTool callback</p>
|
||||
<p>• Fixed worktree creation following a repository-committed symlink at .claude/worktrees, which could create files outside the repository</p>
|
||||
<p>• Fixed a continue:false hook's halt being dropped when the tool fails or completes mid-stream, and hook infrastructure errors being misreported as user rejections</p>
|
||||
<p>• Fixed SIGTERM during a running Bash tool orphaning the command's process tree in print/SDK mode; the CLI now aborts the turn, kills the tree, and exits 143</p>
|
||||
<p>• Fixed /background and claude --bg failing with "EUNKNOWN: unknown error, uv_spawn" on Windows when Group Policy blocks PowerShell 5.1; the daemon now prefers PowerShell 7</p>
|
||||
<p>• Fixed shell mode (!) not executing commands containing file paths while the path autocomplete popup was open</p>
|
||||
<p>• Fixed auto-mode denial notifications rendering broken characters when a long denial reason was truncated mid-emoji</p>
|
||||
<p>• Fixed Ctrl+J not inserting a newline in the agent view dispatch input on terminals with extended key reporting, and surfaced the newline shortcut in the ? help overlay</p>
|
||||
<p>• Fixed /ultrareview rejecting PR references like #123, PR 123, and pasted PR URLs; error hints now name the command you actually typed</p>
|
||||
<p>• Fixed /ultrareview &lt;branch&gt; not fetching the branch from origin when it exists remotely; it now suggests the closest branch name on typos</p>
|
||||
<p>• Fixed /ultrareview skipping the billing confirmation in a new conversation after /clear</p>
|
||||
<p>• Fixed /ultrareview's "not a git repository" error on Claude Desktop now suggesting the project's repository folder instead of terminal commands</p>
|
||||
<p>• Fixed hosted (host-managed) sessions failing at startup when repository settings configured mTLS certs, extra CA bundles, or OAuth scopes; these transport settings are now ignored with a warning</p>
|
||||
<p>• Fixed a spurious "File has not been read yet" error when editing a file that had been read with offset/limit before resuming a session</p>
|
||||
<p>• Fixed ExitWorktree failing with "no active EnterWorktree session" after resuming a session with --continue/--resume in print/SDK mode</p>
|
||||
<p>• Fixed the workflow agent grid staying empty for Remote Control clients that join a session mid-run</p>
|
||||
<p>• Fixed streaming-mode control requests being marked complete before their handler finished, which could lose the request on session restart</p>
|
||||
<p>• Fixed background sessions created with /fork losing their live-parent protection after a state write failure</p>
|
||||
<p>• Fixed reopening a stopped background session from the agent view failing silently — it now resumes the session, or shows why it can't and lets you force a restart</p>
|
||||
<p>• Fixed agent teams: a stopping teammate could send the leader duplicate idle notifications when team initialization re-ran within a session</p>
|
||||
<p>• Fixed the plan-approval dialog footer splitting "ctrl+g to edit in &lt;editor&gt;" apart when the file path is long</p>
|
||||
<p>• Fixed the welcome banner keeping its old panel widths after a combined width+height terminal resize in fullscreen mode</p>
|
||||
<p>• Fixed diff previews losing their line numbers and +/- markers in narrow layouts</p>
|
||||
<p>• Fixed @-mentions attaching nothing after a partial file read, plugin uninstall targeting the wrong marketplace, and false "Command timed out" on exit code 143</p>
|
||||
<p>• Fixed OpenTelemetry HTTP exports being rejected with 411/400 by Azure Monitor and other endpoints that don't accept chunked transfer encoding</p>
|
||||
<p>• Fixed OTLP event log records missing trace_id/span_id when TRACEPARENT is set in SDK/headless mode</p>
|
||||
<p>• Fixed conversations with many images incorrectly failing with "Request too large" errors, and improved the error message to explain the actual cause</p>
|
||||
<p>• Fixed web search and web fetch returning "API Error" text as search results or page content when the API was overloaded</p>
|
||||
<p>• Improved web search and web fetch reliability by retrying 529 errors and rate-limited requests with bounded backoff</p>
|
||||
<p>• Improved prompt caching: the mid-conversation system block now works behind LLM gateways and custom base URLs (Bedrock, Vertex, 1P)</p>
|
||||
<p>• Improved background agent attach: cold-attaching now instantly shows the formatted transcript while the session boots, instead of a blank wait</p>
|
||||
<p>• Reduced token usage in inter-agent messaging: SendMessage bodies are no longer duplicated into replayed history and tool results</p>
|
||||
<p>• Changed /fork to name the copy after your prompt when the session has no title, so the row is recognizable in the agent view</p>
|
||||
<p>• Changed bare /btw to reopen the side-question panel on your most recent exchange so you can browse earlier answers</p>
|
||||
<p>• Changed the ← footer hint to pulse N done for a moment when a background agent finishes while nothing needs your input</p>
|
||||
<p>• Deprecated the Task tool's mode parameter (now ignored); subagents inherit the parent session's permission mode by default</p>
|
||||
<p>• Changed Enterprise forceLoginMethod to be enforced for VS Code extension, SDK, setup-token, and install-github-app logins, not just the terminal</p>
|
||||
<p>• Changed session transcripts to record the reasoning effort level on each assistant message</p>
|
||||
<p>• Changed headless/SDK sessions to apply a set_model control request mid-turn; the next model round-trip uses the new model instead of waiting for the next turn</p>
|
||||
<p>• Changed agent view / claude agents --json: sessions waiting on a sandbox, MCP-input, or managed-settings prompt now show as "Needs input" instead of "Working"</p>
|
||||
<p>• Updated the auth status panel title from "Cloud authentication" to "Authentication"</p>
|
||||
<p>• Corrected an earlier release note (2.1.200): tmux through the 3.6 series lacks synchronized output; newer tmux with support is detected automatically</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.211</id>
|
||||
<title>Claude Code v2.1.211</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.211"/>
|
||||
<updated>2026-07-15T23:02:29Z</updated>
|
||||
<content type="html"><p>• Added --forward-subagent-text flag and CLAUDE_CODE_FORWARD_SUBAGENT_TEXT environment variable to include subagent text and thinking in stream-json output</p>
|
||||
<p>• Fixed permission previews relayed to chat channels not neutralizing bidirectional-override, zero-width, and look-alike quote characters, so tool inputs cannot visually alter the approval message</p>
|
||||
<p>• Fixed auto mode overriding a PreToolUse hook's ask decision for unsandboxed Bash — a hook ask now floors the decision at a prompt</p>
|
||||
<p>• Fixed parallel Claude Code sessions all logging out simultaneously after wake-from-sleep when many sessions share one credential store</p>
|
||||
<p>• Fixed plugin MCP servers not reconnecting after an idle web session woke, leaving MCP calls failing until the next message</p>
|
||||
<p>• Fixed Claude Code on Vertex and Bedrock attempting the default Opus model at startup and printing a spurious fallback notice when a model is explicitly configured</p>
|
||||
<p>• Fixed subagents spawned with an explicit model override reverting to the parent's model when resumed or sent a follow-up message</p>
|
||||
<p>• Fixed nested .claude/rules/*.md files loading even when setting sources exclude project settings</p>
|
||||
<p>• Fixed file upload validation: filenames ending in a DOS device suffix (.prn) or trailing dot are now accepted, and files with multiple hard links are refused</p>
|
||||
<p>• Fixed file uploads to Claude in Chrome from remote and CLI sessions</p>
|
||||
<p>• Fixed edits that leave the input as "?" being silently swallowed and toggling the shortcuts panel</p>
|
||||
<p>• Fixed a startup hang when the Claude in Chrome extension is enabled but Chrome is not running</p>
|
||||
<p>• Fixed a 300ms delay revealing async content (Settings tabs, Stats, diff views, and other loading states)</p>
|
||||
<p>• Fixed reopening a just-stopped background session from the agents view starting a blank conversation under the same session id</p>
|
||||
<p>• Fixed /loop hiding the session from /resume after a single use</p>
|
||||
<p>• Fixed screen reader users losing the audible terminal bell after /terminal-setup or onboarding terminal setup</p>
|
||||
<p>• Fixed background jobs on LLM gateway auth (ANTHROPIC_AUTH_TOKEN + ANTHROPIC_BASE_URL) coming back "Not logged in" after the daemon respawns them</p>
|
||||
<p>• Fixed claude agents jobs becoming permanently undeletable when git no longer recognizes their worktree — the row now shows why the delete was refused instead of silently reappearing</p>
|
||||
<p>• Fixed /clear not resetting the session cost counter — the statusline's cost now starts at $0 after /clear</p>
|
||||
<p>• Fixed Claude in Chrome setup pages failing to open in the browser on Windows</p>
|
||||
<p>• Fixed headless print-mode sessions on Windows crashing or silently exiting when stdin is unreadable</p>
|
||||
<p>• Fixed background session titles in the agents view showing the naming model's refusal text when the prompt contains a link</p>
|
||||
<p>• Fixed background agents killed by the user auto-respawning, and revived agents re-running stale prompts from old sessions</p>
|
||||
<p>• Fixed routines with no schedule reporting a next run time in the year 1</p>
|
||||
<p>• Hardened synced skill/plugin directory naming on Windows and kept CCR web fetch/search proxies working after /clear</p>
|
||||
<p>• Improved terminal layout and rendering performance</p>
|
||||
<p>• Improved background agent result reporting — Claude now reports the status of still-running agents and waits for the real completion instead of fabricating results</p>
|
||||
<p>• Improved the memory index over-limit warning to measure only loaded content, excluding frontmatter and HTML comments</p>
|
||||
<p>• Updated integer environment variables (timeouts, token budgets, retry counts) to accept scientific notation and digit-separator spellings like 1e6 and 64_000</p>
|
||||
<p>• Updated documentation links to the current docs sites</p>
|
||||
<p>• Changed "always allow" permission rules to save at the repository root, so approvals granted in a git worktree persist across sessions and worktrees</p>
|
||||
<p>• Changed /usage-credits to ask for confirmation before sending a request to organization admins</p>
|
||||
<p>• Changed Vim mode s and S (substitute char/line) to work in NORMAL mode, matching vim behavior</p>
|
||||
<p>• [VSCode] Updated the Remote Control banner to describe what it does</p>
|
||||
<p>• Claude in Chrome: hardened file-upload path validation</p>
|
||||
<p>• Claude in Chrome: save_to_disk on screenshot actions now writes the image to disk and returns the path; previously it did nothing</p>
|
||||
<p>• Fixed a prompt-caching regression on Bedrock, Vertex, Mantle, and Foundry that billed the trailing system context block as fresh input tokens on every request.</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.210</id>
|
||||
<title>Claude Code v2.1.210</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.210"/>
|
||||
<updated>2026-07-14T23:45:19Z</updated>
|
||||
<content type="html"><p>• Added a live elapsed-time counter to the collapsed tool summary line so long-running tool calls visibly tick instead of looking stuck</p>
|
||||
<p>• Added a startup warning for Write(path), NotebookEdit(path), and Glob(path) permission rules — use Edit(path) or Read(path) instead</p>
|
||||
<p>• Fixed isolation: 'worktree' subagents being able to run git-mutating commands against the main repo checkout instead of their own isolated worktree</p>
|
||||
<p>• Fixed the ultracode keyword opt-in firing on non-human-originated input such as webhook payloads and relayed PR comments</p>
|
||||
<p>• Fixed a rendered text fragment leaking into crash telemetry when a UI component returned content outside a styled text element</p>
|
||||
<p>• Fixed paste markers leaking into external editors opened from Claude Code, which could appear as stray È/É characters around pasted text</p>
|
||||
<p>• Fixed claude attach sometimes failing with "job not found" or "agent is still starting" errors during session transitions — attach now waits for the daemon to settle, and terminal resizes during a slow attach are applied once it completes</p>
|
||||
<p>• Fixed a session crash when a tool's result renderer returned a numeric bigint value or plain text instead of a UI element</p>
|
||||
<p>• Fixed a hook callback timeout being misreported to the model as a user rejection, which made unattended sessions stop and wait</p>
|
||||
<p>• Fixed Claude assuming a cd took effect after its command was moved to the background; the tool result now states the working directory is unchanged</p>
|
||||
<p>• Fixed plugin-provided MCP servers being torn down when MCP servers are re-synced mid-session</p>
|
||||
<p>• Fixed plan approvals without edits being labeled "(edited by user)" and overwriting the plan file with a stale snapshot</p>
|
||||
<p>• Fixed /doctor skipping its auto-mode-default proposal on Bedrock, Vertex, and Foundry, where auto mode no longer needs an opt-in</p>
|
||||
<p>• Fixed Grep content mode claiming "No matches found" when paginating past the end of results</p>
|
||||
<p>• Fixed unmatched $1/$2 positional placeholders in skills and commands being silently stripped; they are now preserved verbatim</p>
|
||||
<p>• Fixed plugin cache writes leaving temp files behind on failure and failing on locked-file renames on Windows and network filesystems</p>
|
||||
<p>• Fixed background workers crash-looping when a client resets its connection to the background service</p>
|
||||
<p>• Fixed claude agents --effort ultracode not reaching dispatched sessions; the value was silently dropped</p>
|
||||
<p>• Fixed pressing ← to open the agents view dropping the task tracker when returning to the session</p>
|
||||
<p>• Fixed the agents dashboard retaining pasted images from abandoned reply drafts after their session was deleted</p>
|
||||
<p>• Fixed killed background sessions leaving a permanent git worktree lock behind; the periodic sweep now releases locks whose owning process is gone</p>
|
||||
<p>• Fixed SDK MCP servers registered via an initialize control request waiting until the next turn to start connecting</p>
|
||||
<p>• Fixed returning to the agents view from a session leaving overlapping ghost frames with CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1</p>
|
||||
<p>• Fixed late-appearing .claude/* symlinks not being reconciled into the sandbox deny-write list</p>
|
||||
<p>• Hardened the Agent tool against indirect prompt injection via content a subagent read</p>
|
||||
<p>• Improved the Bash/PowerShell tool message when a command hits its timeout and is auto-backgrounded, so the model can distinguish a hang from an explicit background request</p>
|
||||
<p>• Improved auto mode: the permission classifier now defaults to Sonnet 5 for external sessions, validated on the session's first request and pinned for the session</p>
|
||||
<p>• Improved the bundled dataviz skill's chart color validation with perceptual OKLab color difference and recalibrated color-blindness thresholds</p>
|
||||
<p>• Memory writes that leave a MEMORY.md index over its read limit now produce an explicit error instead of silent truncation</p>
|
||||
<p>• Screen reader mode now announces permission mode changes aloud when cycling modes with Shift+Tab</p>
|
||||
<p>• The agents footer hint now shows how many background agents are waiting on your input, with a brief color emphasis when the count changes</p>
|
||||
<p>• Agent view: the session you pressed ← from stays visibly marked even after mouse hover or arrow keys move the selection</p>
|
||||
<p>• Fable temporarily shows as unavailable in the advisor picker while a server-side issue causing Fable advisor failures is fixed</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.209</id>
|
||||
<title>Claude Code v2.1.209</title>
|
||||
@@ -23,6 +291,7 @@
|
||||
<p>• Added vimInsertModeRemaps setting: map two-key insert-mode sequences like jj to Escape in vim mode</p>
|
||||
<p>• Added CLAUDE_CODE_PROCESS_WRAPPER: agent view and the background service now honor a corporate launcher by running every Claude Code self-spawn through a required wrapper executable</p>
|
||||
<p>• Added mouse-click support for multi-select menus and "Other" input rows in fullscreen mode</p>
|
||||
<p>• Changed the Fable 5 usage-credits consent prompt to start with the decline option focused</p>
|
||||
<p>• Fixed fast mode staying off after switching back to a model that supports it — it now restores automatically when enabled in settings</p>
|
||||
<p>• Fixed replies typed to a background agent being lost when delivery fails — the text is now saved and delivered when the session restarts</p>
|
||||
<p>• Fixed background-session attach failing permanently ("Couldn't start the background daemon") after an update replaced the binary a running claude agents process was launched from</p>
|
||||
@@ -337,175 +606,4 @@
|
||||
<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>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.195"/>
|
||||
<updated>2026-06-26T21:29:36Z</updated>
|
||||
<content type="html"><p>• Added CLAUDE_CODE_DISABLE_MOUSE_CLICKS to disable mouse click/drag/hover in fullscreen mode while keeping wheel scroll</p>
|
||||
<p>• Fixed hook matchers with hyphenated identifiers (e.g. code-reviewer, mcp__brave-search) accidentally substring-matching — they now exact-match. Use mcp__brave-search__.* to match all tools from a hyphenated MCP server.</p>
|
||||
<p>• Fixed voice dictation on macOS capturing silence in long-running sessions after the default input device changes</p>
|
||||
<p>• Fixed voice dictation auto-submit never firing for languages written without spaces (Japanese, Chinese, Thai)</p>
|
||||
<p>• Fixed external plugins enabled only by project .claude/settings.json not requiring explicit install consent on every loader path</p>
|
||||
<p>• Fixed /plugin Enable/Disable not working when a plugin's plugin.json name differs from its marketplace entry name</p>
|
||||
<p>• Fixed background jobs disappearing from claude agents or losing data when written by a newer Claude Code version</p>
|
||||
<p>• Fixed reopening a crashed background task showing a blank screen for up to 5 seconds instead of its restart</p>
|
||||
<p>• Fixed background agent daemons running unreachable when the control socket fails to start, blocking restarts</p>
|
||||
<p>• Improved voice mode on Linux: now distinguishes "no microphone" from "SoX not installed" when SoX is present but no audio capture device exists</p>
|
||||
<p>• Improved claude agents completed list to fill available vertical space; on short terminals the header compacts so live sessions stay visible</p>
|
||||
<p>• Improved Remote session startup with a provisioning checklist while the container starts</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.193</id>
|
||||
<title>Claude Code v2.1.193</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.193"/>
|
||||
<updated>2026-06-25T21:45:51Z</updated>
|
||||
<content type="html"><p>• Added autoMode.classifyAllShell setting to route all Bash/PowerShell commands through the auto-mode classifier instead of only arbitrary-code-execution patterns</p>
|
||||
<p>• Added auto-mode denial reasons to the transcript, the denial toast, and /permissions recent denials</p>
|
||||
<p>• Added claude_code.assistant_response OpenTelemetry log event containing the model's response text. Redacted unless OTEL_LOG_ASSISTANT_RESPONSES=1; when that var is unset it follows OTEL_LOG_USER_PROMPTS, so deployments that already log prompt content will start receiving response content on upgrade — set OTEL_LOG_ASSISTANT_RESPONSES=0 to keep prompts-only.</p>
|
||||
<p>• Added live file path autocomplete to bash mode (!)</p>
|
||||
<p>• Added a startup notice when MCP servers need authentication, pointing at /mcp</p>
|
||||
<p>• Added automatic memory-pressure reaping for idle background shell commands (disable with CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP=1)</p>
|
||||
<p>• Fixed /model and other client-data-gated UI showing stale/empty state immediately after /login</p>
|
||||
<p>• Fixed backgrounding (←←) spuriously cancelling with "N background tasks would be abandoned" when all running tasks carry over to the new session</p>
|
||||
<p>• Fixed pinned background agents being re-prompted to "Continue from where you left off" after every auto-update</p>
|
||||
<p>• Fixed backgrounding the main turn spawning a phantom "general-purpose (resumed)" subagent that re-ran the main conversation</p>
|
||||
<p>• Fixed agent panel hiding sibling agents when viewing a subagent</p>
|
||||
<p>• Improved background agents: the launch result no longer instructs Claude to "end your response" — it keeps working on other tasks while the agent runs</p>
|
||||
<p>• Improved MCP headersHelper auth: the helper now re-runs and reconnects automatically when a tool call returns 401/403</p>
|
||||
<p>• Improved plugin auto-rename: marketplace renames maps are now followed automatically, updating your settings to the new name</p>
|
||||
<p>• Improved /add-dir message when the directory is already a working directory</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.191</id>
|
||||
<title>Claude Code v2.1.191</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.191"/>
|
||||
<updated>2026-06-24T21:58:06Z</updated>
|
||||
<content type="html"><p>• Added /rewind support for resuming a conversation from before /clear was run</p>
|
||||
<p>• Fixed scroll position jumping to the bottom while reading earlier output during a streaming response</p>
|
||||
<p>• Fixed background agents resurrecting after being stopped — stopping an agent from the tasks panel is now permanent</p>
|
||||
<p>• Fixed /voice showing a generic "not available" message when disabled by an organization's policy — it now explains the restriction</p>
|
||||
<p>• Fixed /login URL opening truncated in Windows Terminal when it wraps across lines</p>
|
||||
<p>• Fixed Cmd+click on links in fullscreen mode for Ghostty over ssh/tmux</p>
|
||||
<p>• Fixed claude agents sending builtin slash commands like /usage to background sessions as prompt text instead of showing a hint</p>
|
||||
<p>• Fixed claude agents job rows showing full filesystem paths for pasted images instead of the [Image #N] placeholder</p>
|
||||
<p>• Fixed hooks with comma-separated matchers (e.g. "Bash,PowerShell") silently never firing</p>
|
||||
<p>• Fixed /permissions Recently-denied tab: approving a denial now persists on close instead of being silently discarded</p>
|
||||
<p>• Fixed the agent panel jumping by one row when scrolling the roster past the overflow cap</p>
|
||||
<p>• Fixed the welcome splash art overflowing the default 80×24 macOS Terminal window</p>
|
||||
<p>• Fixed managed settings: forceRemoteSettingsRefresh now takes effect when set via MDM or file policy, and the fetch sends Cache-Control: no-cache to prevent proxies from serving stale responses</p>
|
||||
<p>• Improved sandbox network permission dialog: hosts you allow with "Yes" are now remembered for the rest of the session instead of re-prompting on every connection</p>
|
||||
<p>• Improved MCP server reliability: capability discovery (tools/list, prompts/list, resources/list) now retries transient network errors with short backoff</p>
|
||||
<p>• Improved MCP OAuth: discovery and token requests now retry once after transient network errors, and headless environments skip the browser popup and go straight to the paste-the-URL prompt</p>
|
||||
<p>• Improved MCP error messages: HTTP 404 errors now show the URL and point to your MCP config</p>
|
||||
<p>• Improved vim mode prompt-history search (NORMAL /) to hint how to reach slash commands</p>
|
||||
<p>• Reduced CPU usage during streaming responses by ~37% by coalescing text updates to 100ms</p>
|
||||
<p>• Reduced long-session memory growth from terminal output cache</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.190</id>
|
||||
<title>Claude Code v2.1.190</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.190"/>
|
||||
<updated>2026-06-24T15:53:48Z</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.187</id>
|
||||
<title>Claude Code v2.1.187</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.187"/>
|
||||
<updated>2026-06-23T21:03:40Z</updated>
|
||||
<content type="html"><p>• Added sandbox.credentials setting to block sandboxed commands from reading credential files and secret environment variables</p>
|
||||
<p>• Added org-configured model restrictions to the model picker, --model, /model, and ANTHROPIC_MODEL, with a "restricted by your organization's settings" message when a restricted model is selected</p>
|
||||
<p>• Added mouse click support to select menus (permission prompts, /model, /config, etc.) in fullscreen mode</p>
|
||||
<p>• Fixed --resume failing with "No conversation found" when the original -p run produced no model turns</p>
|
||||
<p>• Fixed --json-schema and workflow agent({schema}) structured output: the model can no longer re-call StructuredOutput indefinitely after a successful call, and follow-up turns now reliably return structured output</p>
|
||||
<p>• Fixed remote MCP tool calls that hang with no response for 5 minutes — they now abort with an error instead of blocking indefinitely (override with CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT)</p>
|
||||
<p>• Fixed Claude Code Remote sessions taking ~2.7s longer to start after the agent proxy CA system-trust install was added</p>
|
||||
<p>• Fixed pasted Korean/CJK text turning into mojibake in terminals that deliver paste as per-byte extended-key events</p>
|
||||
<p>• Fixed /update over Remote Control hanging when a startup trust dialog would have shown</p>
|
||||
<p>• Fixed background jobs in the agents view getting stuck in "working" indefinitely when the agent ended a turn without producing structured output</p>
|
||||
<p>• Fixed channel connections dropping after navigating to the agents view and back, and after /bg, /tui, or /update</p>
|
||||
<p>• Fixed agent stop notifications not correctly attributing who stopped the agent, and improved wording ("finished"/"stopped" instead of "came to rest")</p>
|
||||
<p>• Fixed subagent depth tracking: resumed subagents now restore their original spawn depth, and forked subagents now count toward the depth cap</p>
|
||||
<p>• Fixed leaked agent worktree registrations: locked .git/worktrees/ entries from killed agents are now cleaned up automatically</p>
|
||||
<p>• Fixed Cmd+click not opening URLs in fullscreen mode in Ghostty on macOS</p>
|
||||
<p>• Fixed claude --help not listing the --bg/--background flag</p>
|
||||
<p>• Fixed Esc, Ctrl-C, and Ctrl-D not working while /share is uploading</p>
|
||||
<p>• Improved /install-github-app: GitHub Actions workflow setup is now optional — you can install just the GitHub App and skip the workflow/secret steps</p>
|
||||
<p>• Improved /btw with ←/→ arrow navigation to step through earlier answers</p>
|
||||
<p>• Improved /plugin to surface plugins you haven't used recently so you can clean them up</p>
|
||||
<p>• [VSCode] Fixed extension becoming unresponsive when resuming a large session</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>https://github.com/anthropics/claude-code/releases/tag/v2.1.186</id>
|
||||
<title>Claude Code v2.1.186</title>
|
||||
<link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.186"/>
|
||||
<updated>2026-06-22T20:37:26Z</updated>
|
||||
<content type="html"><p>• Added claude mcp login &lt;name&gt; and claude mcp logout &lt;name&gt; to authenticate MCP servers from the CLI without opening the interactive /mcp menu, with --no-browser stdin redirect support for completing over SSH</p>
|
||||
<p>• Added status filtering (press f) to the /workflows agent detail view</p>
|
||||
<p>• Added a "Skills" section to the /plugin Installed tab</p>
|
||||
<p>• Added teammateMode: "iterm2" setting with a warning when auto mode cannot find the it2 CLI</p>
|
||||
<p>• Added "Claude Platform on AWS - refresh credentials" option to /login when awsAuthRefresh is configured</p>
|
||||
<p>• ! bash commands now trigger Claude to respond to the output automatically; set "respondToBashCommands": false in settings.json to keep the previous context-only behavior</p>
|
||||
<p>• Fixed streaming requests failing with "Content block not found" or JSON parse errors after the machine wakes from sleep</p>
|
||||
<p>• Fixed subagent transcript scroll position bleeding into the main transcript on exit</p>
|
||||
<p>• Fixed background task previews flashing raw tool names before the agent's plan loaded</p>
|
||||
<p>• Fixed Chrome tab-group isolation not applying when the in-product permissions gate is off for concurrent CLI sessions</p>
|
||||
<p>• Fixed background session recaps being duplicated; the agent's own end-of-turn summary now shows as the recap line</p>
|
||||
<p>• Fixed opening a background session from claude agents leaving the previous screen painted behind it</p>
|
||||
<p>• Fixed Agent(type) deny rules and Agent(x,y) allowed-types restrictions not being enforced for named subagent spawns</p>
|
||||
<p>• Fixed Esc and Ctrl+C not responding while background agents are still running after the main turn ends</p>
|
||||
<p>• Fixed misaligned option numbers in permission prompts when the option text overflows</p>
|
||||
<p>• Fixed pressing x on a finished subagent in the agent panel not dismissing it</p>
|
||||
<p>• Fixed a misleading "MCP server disconnected" notice for intentionally retired tools when resuming older sessions</p>
|
||||
<p>• Fixed /plugin Installed showing a "more above" indicator when already scrolled to the top</p>
|
||||
<p>• Fixed ~~strikethrough~~ showing literal tildes in assistant messages instead of rendering as strikethrough</p>
|
||||
<p>• Fixed --tools allowing feature-gated tools to slip through before flags loaded on a cold first launch</p>
|
||||
<p>• Fixed background job status in claude agents showing a stale "needs input" message after replying</p>
|
||||
<p>• Fixed a dark-theme flash when opening a background session from claude agents on a light terminal</p>
|
||||
<p>• Fixed mouse-selected text staying highlighted after deleting it in claude agents</p>
|
||||
<p>• Fixed session cost not showing for usage-based Enterprise and Team subscribers</p>
|
||||
<p>• Fixed agent teams: teammates spawned via tmux/pane backends now inherit the leader's --effort level</p>
|
||||
<p>• Fixed Workflow agent({schema}) subagents looping forever on repeated schema validation failures instead of aborting after 5 attempts</p>
|
||||
<p>• Improved claude mcp get and claude mcp remove to suggest the closest configured server name on a typo and truncate long server lists</p>
|
||||
<p>• Improved memory: the agent is now reminded to compact its MEMORY.md index when nearing the size limit</p>
|
||||
<p>• Improved skill frontmatter: display-name, default-enabled, fallback, and metadata.* keys now accept kebab-case, snake_case, and camelCase</p>
|
||||
<p>• Improved malformed SKILL.md YAML frontmatter handling: loads the skill body with empty metadata instead of failing silently</p>
|
||||
<p>• Changed CLAUDE_CODE_MAX_RETRIES to cap at 15; for unattended sessions, use CLAUDE_CODE_RETRY_WATCHDOG instead</p>
|
||||
<p>• Changed background subagents to surface permission prompts in the main session instead of auto-denying; the dialog shows which agent is asking, and Esc denies just that tool</p>
|
||||
<p>• Changed /review &lt;pr&gt; to use the same review engine as /code-review medium</p></content>
|
||||
</entry>
|
||||
</feed>
|
||||
|
||||
Reference in New Issue
Block a user