diff --git a/.github/egress-firewall.yaml b/.github/egress-firewall.yaml new file mode 100644 index 0000000000..d11942ce0d --- /dev/null +++ b/.github/egress-firewall.yaml @@ -0,0 +1,27 @@ +# Hosts that jobs on GitHub's egress-firewall runner (runs-on: ubuntu-24.04-firewall) +# may reach. All other hosts are blocked, apart from any that GitHub's firewall allows +# by default. Add a host only when a workflow step needs it, name it in full (no '*'), and say what uses it. +mode: enforce +allow: + # Claude API: model requests, trading the workflow's GitHub identity token for a + # short-lived API token, and fetching the Claude GitHub App's token in claude.yml + - api.anthropic.com + # GitHub API: calls made by the Claude Code action, and by gh in scripts/gh.sh, + # scripts/edit-issue-labels.sh and scripts/comment-on-duplicates.sh + - api.github.com + # Claude Code install script, fetched by the Claude Code action + - claude.ai + # Claude Code binary, downloaded by the install script + - downloads.claude.ai + # Bun release download (oven-sh/setup-bun, used by the Claude Code action) + - release-assets.githubusercontent.com + # npm packages for the Claude Code action (bun install) + - registry.npmjs.org + # apt packages bubblewrap and socat, which the Claude Code action installs when a + # workflow admits users without write access (claude-issue-triage.yml, + # claude-dedupe-issues.yml) + - azure.archive.ubuntu.com + - archive.ubuntu.com + - security.ubuntu.com + # Statsig event logging (claude-dedupe-issues.yml, "Log duplicate comment event to Statsig") + - events.statsigapi.net diff --git a/.github/scripts/check_workflow_hardening.py b/.github/scripts/check_workflow_hardening.py new file mode 100755 index 0000000000..6c2a09ac3f --- /dev/null +++ b/.github/scripts/check_workflow_hardening.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 +"""Fail if a workflow job that calls Claude, or .github/egress-firewall.yaml, breaks a rule in CLAUDE.md, +"Security hardening for GitHub Actions". Run from the repository root. A job calls Claude when it runs the +Claude Code action, or when it or a local action it uses mentions ANTHROPIC_FEDERATION_RULE_ID. +""" + +import json +import pathlib +import re +import shlex +import subprocess +import sys + +FIREWALL_RUNNER = "ubuntu-24.04-firewall" +WORKFLOW_DIR = pathlib.Path(".github/workflows") +POLICY_PATH = pathlib.Path(".github/egress-firewall.yaml") +SIGN_IN_MARKER = "anthropic_federation_rule_id" +CLAUDE_ACTIONS = ("anthropics/claude-code-action", "anthropics/claude-code-base-action") +HELP = 'See CLAUDE.md, "Security hardening for GitHub Actions".' +# Claude Code runs auto mode only on claude-opus-4-6 and newer models. On an older model it +# falls back to its default permission mode, with no safety review. +AUTO_MODE_MIN_VERSION = (4, 6) +# The version in a model name: claude-opus-4-6, claude-sonnet-4-5-20250929, claude-3-5-sonnet-latest. +MODEL_VERSION = re.compile( + r"claude-(?:(?:opus|sonnet|haiku)-(\d+)(?:-(\d{1,2}))?" + r"|(\d+)(?:-(\d{1,2}))?-(?:opus|sonnet|haiku))(?![\d.])" +) + +# Key: ":". Value: why that job is exempt from the table's rule. +EXEMPT_FROM_FIREWALL_RUNNER: dict[str, str] = {} +EXEMPT_FROM_AUTO_MODE: dict[str, str] = {} + + +def stop(message: str): + sys.exit(f"::error::{message}") + + +def load_yaml(path: pathlib.Path): + """Parse a YAML file with PyYAML, or with the yq command if PyYAML is absent.""" + try: + import yaml + except ImportError: + try: + result = subprocess.run( + ["yq", "-o=json", ".", str(path)], check=True, capture_output=True, text=True + ) + except FileNotFoundError: + stop( + f"Cannot read {path}: Python has no 'yaml' module and no 'yq' command was found. " + "Add a step that runs 'pip install pyyaml' before this check." + ) + except subprocess.CalledProcessError: + stop(f"Cannot read {path}: 'yq' could not parse it. Check that the file is valid YAML.") + return json.loads(result.stdout) + try: + with path.open(encoding="utf-8") as handle: + return yaml.safe_load(handle) + except yaml.YAMLError as error: + stop(f"Cannot read {path}: it is not valid YAML ({error}).") + + +def contains_marker(node) -> bool: + """Whether any key or string under node contains SIGN_IN_MARKER, ignoring case.""" + if isinstance(node, dict): + return any(contains_marker(k) or contains_marker(v) for k, v in node.items()) + if isinstance(node, list): + return any(contains_marker(item) for item in node) + return isinstance(node, str) and SIGN_IN_MARKER in node.lower() + + +def load_local_action(uses: str): + """The parsed action file of a local action (uses: ./path), or None.""" + if not uses.startswith("./"): + return None + for name in ("action.yml", "action.yaml"): + action_file = pathlib.Path(uses) / name + if action_file.is_file(): + action = load_yaml(action_file) + return action if isinstance(action, dict) else {} + return None + + +def steps_of(job: dict) -> list[dict]: + return [step for step in job.get("steps") or [] if isinstance(step, dict)] + + +def runs_claude_code_action(step: dict) -> bool: + """Whether the step runs the Claude Code action: the published action, or a + local action that accepts a claude_args input.""" + uses = str(step.get("uses", "")) + if uses.lower().startswith(CLAUDE_ACTIONS): + return True + action = load_local_action(uses) + return action is not None and "claude_args" in (action.get("inputs") or {}) + + +def job_calls_claude(job: dict) -> bool: + if contains_marker(job): + return True + for step in steps_of(job): + if runs_claude_code_action(step): + return True + action = load_local_action(str(step.get("uses", ""))) + if action is not None and contains_marker(action): + return True + return False + + +def permission_mode_problem(step: dict, exempt: bool, inherited_env: dict) -> str | None: + """The message for a step whose permission mode is wrong, or None if it is right. + + A step must set auto mode, on a model that supports it. A step of a job in + EXEMPT_FROM_AUTO_MODE must set no mode at all. inherited_env is the workflow's and the + job's 'env'. + """ + inputs = step.get("with") or {} + lines = str(inputs.get("claude_args", "")).splitlines() + text = " ".join(line for line in lines if not line.strip().startswith("#")) + try: + args = shlex.split(text, comments=True) + except ValueError: + return "'claude_args' has a quote that is never closed. Close it" + modes = [] + for index, arg in enumerate(args): + if arg == "--dangerously-skip-permissions": + return ( + "remove '--dangerously-skip-permissions' from 'claude_args': " + "it turns off permission checks" + ) + if arg == "--permission-mode": + modes.append(args[index + 1] if index + 1 < len(args) else "") + elif arg.startswith("--permission-mode="): + modes.append(arg.split("=", 1)[1]) + if exempt and modes: + return ( + "remove '--permission-mode' from 'claude_args': this job is listed in " + "EXEMPT_FROM_AUTO_MODE (.github/scripts/check_workflow_hardening.py), and a job listed " + "there must not set a permission mode" + ) + if not modes and not exempt: + return "add '--permission-mode auto' to 'claude_args' under the step's 'with:'" + for mode in modes: + if mode == "": + return ( + "'claude_args' has '--permission-mode' with nothing after it. " + "Write '--permission-mode auto'" + ) + if mode != "auto": + return ( + f"'claude_args' has '--permission-mode {mode}'. " + "Change it to '--permission-mode auto'" + ) + env = {**inherited_env, **(step.get("env") or {})} + models = [ + ("the step's 'model'", inputs.get("model", "")), + ("ANTHROPIC_MODEL", env.get("ANTHROPIC_MODEL", "")), + ] + models += [ + (f"'{flag}' in 'claude_args'", value) + for flag in ("--model", "--fallback-model") + for value in flag_values(args, flag) + ] + settings = [("the step's 'settings'", inputs.get("settings", ""))] + settings += [ + ("'--settings' in 'claude_args'", value) for value in flag_values(args, "--settings") + ] + for where, value in settings: + text = settings_text(value) + if text is None: + return ( + f"{where} names a file outside the repository, which this check cannot read. " + "Use inline settings or a file inside the repository" + ) + if "defaultMode" in text: + return f"remove 'defaultMode' from {where}: settings must not set a permission mode" + try: + parsed = json.loads(text) if text else {} + except ValueError: + parsed = {} + if isinstance(parsed, dict) and "model" in parsed: + models.append((f"'model' in {where}", parsed["model"])) + if not exempt: + for where, model in models: + if predates_auto_mode(str(model or "")): + return ( + f"{where} is '{model}', which Claude Code does not run in auto mode: it " + "falls back to the default permission mode. Use claude-opus-4-6 or a newer model" + ) + return None + + +def flag_values(args: list[str], flag: str) -> list[str]: + """The values a flag in claude_args is given, as '--flag value' or '--flag=value'.""" + values = [ + args[index + 1] for index, arg in enumerate(args) if arg == flag and index + 1 < len(args) + ] + return values + [arg.split("=", 1)[1] for arg in args if arg.startswith(f"{flag}=")] + + +def predates_auto_mode(model: str) -> bool: + """Whether the model is older than AUTO_MODE_MIN_VERSION. A name with no version, such as + 'opus' or 'default', stands for a current model, except 'haiku' (claude-haiku-4-5).""" + if model.strip().lower() == "haiku": + return True + match = MODEL_VERSION.search(model.lower()) + if not match: + return False + major, minor = match.group(1, 2) if match.group(1) else match.group(3, 4) + return (int(major), int(minor or 0)) < AUTO_MODE_MIN_VERSION + + +def settings_text(value) -> str | None: + """The settings JSON a 'settings' value stands for: the value itself, or the contents of + the file it names when it is a path inside the repository. None when it names a path + outside the repository.""" + text = str(value or "").strip() + if not text or text.startswith("{"): + return text + path = pathlib.Path(text) + root = pathlib.Path.cwd().resolve() + try: + resolved = path.resolve() + resolved.relative_to(root) + except (OSError, ValueError): + return None + if resolved.is_file(): + return resolved.read_text(encoding="utf-8", errors="replace") + return text + + +def check_job(file_name: str, job_id: str, job: dict, workflow_env: dict) -> list[str]: + key = f"{file_name}:{job_id}" + where = f".github/workflows/{file_name}: job '{job_id}'" + errors = [] + runs_on = job.get("runs-on") + if isinstance(runs_on, list) and len(runs_on) == 1: + runs_on = runs_on[0] + if key in EXEMPT_FROM_FIREWALL_RUNNER: + print( + f"The egress-firewall runner is not required for job '{job_id}' in {file_name}. " + f"Reason: {EXEMPT_FROM_FIREWALL_RUNNER[key]}." + ) + elif runs_on != FIREWALL_RUNNER: + if "runs-on" not in job: + has = "no 'runs-on'" + elif isinstance(job["runs-on"], str): + has = f"'runs-on: {job['runs-on']}'" + else: + has = "a 'runs-on' list or group" + errors.append( + f"{where} calls Claude, so it must have 'runs-on: {FIREWALL_RUNNER}'. " + f"It has {has}. {HELP}" + ) + exempt = key in EXEMPT_FROM_AUTO_MODE + if exempt: + print( + f"Auto permission mode is not required for job '{job_id}' in {file_name}. " + f"Reason: {EXEMPT_FROM_AUTO_MODE[key]}." + ) + if "defaultMode" in json.dumps(job): + # Catches a settings file that an earlier step of the job writes, which the + # step-level check cannot read. + errors.append( + f"{where} mentions 'defaultMode': settings must not set a permission mode. {HELP}" + ) + for index, step in enumerate(steps_of(job), start=1): + if not runs_claude_code_action(step): + continue + inherited_env = {**workflow_env, **(job.get("env") or {})} + problem = permission_mode_problem(step, exempt, inherited_env) + if problem: + step_label = f"step '{step['name']}'" if "name" in step else f"step {index}" + errors.append(f"{where}, {step_label}: {problem}. {HELP}") + return errors + + +def check_policy() -> list[str]: + if not POLICY_PATH.is_file(): + return [ + f"{POLICY_PATH} is missing. Jobs on the egress-firewall runner need it " + f"to limit outbound network access. {HELP}" + ] + policy = load_yaml(POLICY_PATH) + if not isinstance(policy, dict): + return [ + f"{POLICY_PATH} is empty or is not a set of 'name: value' lines. It needs 'mode: enforce' " + f"and an 'allow:' list of hosts. {HELP}" + ] + errors = [] + if "mode" not in policy: + errors.append(f"{POLICY_PATH}: 'mode' is missing. Add 'mode: enforce'. {HELP}") + elif policy["mode"] != "enforce": + errors.append( + f"{POLICY_PATH}: 'mode' is '{policy['mode']}'. It must be 'enforce'. {HELP}" + ) + allow = policy.get("allow") + if not isinstance(allow, list) or not allow: + errors.append( + f"{POLICY_PATH}: the 'allow' list is missing or empty. List under 'allow:' " + f"each host the jobs need. {HELP}" + ) + else: + for host in allow: + if "*" in str(host): + errors.append( + f"{POLICY_PATH}: the 'allow' entry '{host}' contains '*'. " + f"Name each host in full. {HELP}" + ) + return errors + + +def main() -> int: + if not WORKFLOW_DIR.is_dir(): + stop(f"{WORKFLOW_DIR} not found. Run this check from the repository root.") + errors = [] + checked = 0 + for path in sorted([*WORKFLOW_DIR.glob("*.yml"), *WORKFLOW_DIR.glob("*.yaml")]): + workflow = load_yaml(path) + jobs = workflow.get("jobs") if isinstance(workflow, dict) else None + for job_id, job in (jobs or {}).items(): + if not isinstance(job, dict) or not job_calls_claude(job): + continue + checked += 1 + errors.extend(check_job(path.name, job_id, job, workflow.get("env") or {})) + if checked: + errors.extend(check_policy()) + for error in errors: + print(f"::error::{error}") + if errors: + return 1 + if checked == 0: + print("OK: no workflow job calls Claude, so there was nothing to check.") + else: + print(f"OK: checked {checked} job(s) that call Claude and found no problems.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/claude-dedupe-issues.yml b/.github/workflows/claude-dedupe-issues.yml index cfbdf2db23..dfdd1fdf25 100644 --- a/.github/workflows/claude-dedupe-issues.yml +++ b/.github/workflows/claude-dedupe-issues.yml @@ -12,7 +12,9 @@ on: jobs: claude-dedupe-issues: - runs-on: ubuntu-latest + # This job calls Claude, so it runs on GitHub's egress-firewall runner, which + # filters the job's outbound network traffic (allow list: .github/egress-firewall.yaml). + runs-on: ubuntu-24.04-firewall timeout-minutes: 10 permissions: contents: read @@ -40,7 +42,10 @@ jobs: anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }} anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }} anthropic_workspace_id: ${{ vars.ANTHROPIC_WORKSPACE_ID }} - claude_args: "--model claude-sonnet-4-5-20250929" + # --permission-mode auto: a tool call that needs permission and is outside the allowed-tools + # list in .claude/commands/dedupe.md runs only if Claude Code's safety review passes it. + # Tools in --disallowedTools never run. + claude_args: '--model claude-sonnet-4-6 --permission-mode auto --disallowedTools "WebFetch,WebSearch,Write,Edit,MultiEdit,NotebookEdit"' - name: Log duplicate comment event to Statsig if: always() diff --git a/.github/workflows/claude-issue-triage.yml b/.github/workflows/claude-issue-triage.yml index 6c667e2d8a..765a5cd854 100644 --- a/.github/workflows/claude-issue-triage.yml +++ b/.github/workflows/claude-issue-triage.yml @@ -7,7 +7,9 @@ on: jobs: triage-issue: - runs-on: ubuntu-latest + # This job calls Claude, so it runs on GitHub's egress-firewall runner, which + # filters the job's outbound network traffic (allow list: .github/egress-firewall.yaml). + runs-on: ubuntu-24.04-firewall timeout-minutes: 10 if: >- github.event_name == 'issues' || @@ -43,5 +45,10 @@ jobs: anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }} anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }} anthropic_workspace_id: ${{ vars.ANTHROPIC_WORKSPACE_ID }} + # --permission-mode auto: a tool call that needs permission and is outside the allowed-tools + # list in .claude/commands/triage-issue.md runs only if Claude Code's safety review passes + # it. Tools in --disallowedTools never run. claude_args: | + --permission-mode auto + --disallowedTools "WebFetch,WebSearch,Write,Edit,MultiEdit,NotebookEdit" --model claude-opus-4-6 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index e2348761f1..3e418ed18c 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -17,7 +17,9 @@ jobs: (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest + # This job calls Claude, so it runs on GitHub's egress-firewall runner, which + # filters the job's outbound network traffic (allow list: .github/egress-firewall.yaml). + runs-on: ubuntu-24.04-firewall permissions: contents: read pull-requests: read @@ -40,5 +42,8 @@ jobs: anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }} anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }} anthropic_workspace_id: ${{ vars.ANTHROPIC_WORKSPACE_ID }} - claude_args: "--model claude-sonnet-4-5-20250929" + # --permission-mode auto: a tool call that needs permission and that the allowed tools + # do not cover runs only if Claude Code's safety review passes it. The action sets + # --permission-mode acceptEdits for @claude mentions, and this one, which comes after it, wins. + claude_args: "--model claude-sonnet-4-6 --permission-mode auto" diff --git a/.github/workflows/workflow-hardening.yml b/.github/workflows/workflow-hardening.yml new file mode 100644 index 0000000000..92f8651a25 --- /dev/null +++ b/.github/workflows/workflow-hardening.yml @@ -0,0 +1,23 @@ +# Fails when a workflow job that calls Claude lacks its security settings. +# The rules are in CLAUDE.md, "Security hardening for GitHub Actions". +name: Security check for workflows that call Claude + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + workflow-hardening: + name: Check security settings + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Check security settings of workflows that call Claude + run: python3 .github/scripts/check_workflow_hardening.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6730210f18..7df81e6a88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,823 @@ # Changelog +## 2.1.294 + +- Fixed `prompt` and `agent` hooks written as instructions (such as "Block commands that...") allowing what they should block +- Improved how `prompt` hooks on Stop and SubagentStop written as instructions (such as "Carry on if the build is broken") are judged, so Claude is less likely to stop early + +## 2.1.293 + +- Added Claude Haiku 5.5 (`claude-haiku-5-5`), now the default Haiku model on the Anthropic API — 1M context, $0.10/$0.50 per Mtok ($0.50/$2.50 for prompts over 100K) +- Added `agentType` to the `subagentStatusLine` payload, so scripts can tell custom subagent types apart +- Added `isDeferred` to `$.tool.register` for mods: `false` lists the tool's schema in the prompt from the start instead of behind tool search +- Fixed Claude sometimes treating its own last actions before a context compaction as done after it, and retracting or redoing finished work +- Fixed a memory leak where an HTTP MCP connection kept every request it had sent until it closed +- Fixed a message sent while Claude was working being lost when `←` moved the session to the background; if a queued message can't move, `←` now stays put and says so +- Fixed `/model` effort ←/→ wrapping past the highest or lowest level, which could accidentally save Low as a model's default effort +- Fixed `/tui` disconnecting Claude in Chrome in a session started with `--chrome`, and ignoring `--no-chrome` +- Fixed Claude being told to continue or message subagents with `SendMessage` in sessions, including resumed ones, where a host, a permission rule or a `--tools` list removed that tool +- Fixed subagents and `--agent` sessions being told a built-in tool was disabled for the whole session when only their own tool list left it out +- Fixed a claude.ai-synced skill's edited description sometimes not reaching the model until a new conversation or `/clear` +- Fixed `claude logs`, `stop`, `kill`, `rm` and `claude daemon status`, `stop`, `uninstall` sometimes signing you out when your login had expired or was about to +- Fixed the footer's agents count disappearing after a momentary failure to read the sessions folder (for example, too many open files) +- Fixed a custom agent named `worker` being shown as "Agent" when it starts, and the agent detail dialog's title losing the agent type once the agent finishes +- Fixed the Artifact tool's transcript row briefly reading `Artifact("(unprintable path)")` while a publish call was still streaming in +- Fixed the `/ultrareview` upload on Linux wrongly refusing some repositories, such as one inside another checkout, over a settings file that "could not be parsed" while a sandboxed command was running +- Fixed the `/ultrareview` upload's refusal over split-index files advising a git command that could leave git unable to read its index +- Fixed replies in very long Remote Control and cloud sessions that could still appear a block at a time instead of streaming in +- Fixed Remote Control uploading a session's starting history again after every credential recovery +- Fixed PushNotification reporting "Remote Control inactive" in sessions started with `claude remote-control` +- Fixed a mod's hooks on `classic.*` events being skipped while the plugin hooks worker restarts, which left settings hooks to answer without them +- Fixed `claude plugin test` failing for mods that call `$.session.append`; tests can read the appended rows back with the new `mock.session` +- Fixed `claude plugin eval` refusing every Bash-granting run on Macs with Docker Desktop (links under `~/.docker/bin`); the refusal now names which part of a credential store held the link +- Fixed the Claude apps gateway refusing to start when a `desktop` policy sets Claude Desktop's built-in browser keys, such as `builtinBrowserEnabled` +- Fixed `claude agents` offering bypass permissions that background sessions then ignored when consent was saved only in `.claude/settings.local.json` or a `--settings` file; it now asks for consent first, and a session that ignores bypass shows a short notice that stays +- Fixed `←` backgrounding a session after 10 seconds while the prompt held unsent text (it now cancels the move) or a question was waiting for your answer +- Fixed Esc, or No without feedback, on a permission prompt not stopping the turn when `←` had just been pressed to move the session to the background +- Fixed the agents view briefly replacing the session list with placeholder rows when the sessions folder could not be read (for example, too many open files) +- Fixed path-scoped rules and nested CLAUDE.md files not loading when Claude views a file with a single-file cat, head, tail, sed -n or grep command in the Bash tool instead of the Read tool +- Fixed pasted text that begins and ends with the same words sometimes being sent to Claude as if it had been typed +- Fixed a skill name in pasted text being treated as typed when an accent typed or pasted right after the paste merged into its last letter +- Fixed `/feedback` returning to the drafts list after Ctrl+O or Ctrl+Z while a report was sending, leaving the send impossible to cancel +- Fixed `claude purge` stopping silently (exit 0, or a hang in a terminal) when a file or folder could not be deleted; it now deletes the rest, lists what it could not delete, and exits 1 +- Fixed keybindings.json checks: a lone " " (space key) is no longer reported as an error, and keys like "ctrl+ k" now get a warning +- Fixed vim mode `>>` and `<<` on a line of only spaces leaving the cursor past the end of the line, where a following `x` deleted nothing +- Fixed vim mode: after deleting whole lines in Visual mode (`V` then `d`) the cursor lands on the first non-blank, and `.` after it acts on the cursor's line +- Windows: Fixed stopping a status line, hook, or shell command sometimes terminating an unrelated process that had been given the same process ID +- Reverted the auto mode denial message change from 2.1.281 that told Claude a denial covers the outcome, not only the exact command +- Reverted the 2.1.290 fix for cloud sessions staying asleep after a container restart lost a pending `/loop` wakeup or scheduled task; Claude is no longer told, and the session stays asleep +- Improved startup for Team and Enterprise organizations: policy and managed settings are fetched earlier, and a stalled request is retried after 3 seconds +- Improved Claude in Chrome: fewer page actions are refused when the browser is slow to report its tabs +- Improved the Claude in Chrome message in cloud sessions when the browser can't be reached: if you belong to more than one organization, it now says the extension must be signed in to the same one, and how to change that +- Improved the Bash edit diff note to say the listed files changed while the command ran, which can include writes by other processes +- Improved artifacts: Claude pins libraries to exact versions two weeks old or older +- Changed claude.ai skill syncing to check for changes about every 40 minutes, instead of every 10, while no session is in use +- Changed the order of agent lists and of MCP servers announced to the model: names with non-ASCII characters now sort after ASCII names +- Changed OpenTelemetry `claude_code.at_mention` logging to emit at most 100 agent and 100 MCP-resource events each time a prompt is read +- Self-hosted runner: Changed the orchestrator to sleep 4 to 6 seconds between polls instead of a constant 5, so several replicas for one environment stop polling at the same moment +- [Claude Tag] Fixed Claude in Slack saying a workspace isn't set up in Enterprise Grid channels shared across workspaces when Slack labels a message with a workspace that isn't connected +- [Claude Tag] Fixed Claude in Slack stopping mid-task in a channel when an admin changed the channel's connectors, plugins, skills or rules; the change now applies once Claude finishes +- [Claude Tag] Fixed asking Claude in Slack to run a thread's routine now with extra notes starting a separate run that couldn't post back; the run now continues in that thread +- [Claude Tag] Fixed an access bundle's Add a connector dialog in Claude Tag admin settings showing every Google connector as connected after one Google sign-in +- [Claude Tag] Improved Claude Tag admin settings to list an Enterprise Grid that isn't connected yet, with a Connect button +- [Claude Tag] Changed Claude in Slack to join announcement channels, where only admins can post, without posting its intro +- [Claude Tag] Changed the limit on channel rules in Claude Tag admin settings from 20 to 50 for each workspace and for the organization-wide Slack page +- [Code Review] Improved Code Review's Add a repository dialog to list each repository that couldn't be added and why, such as missing GitHub write access + +## 2.1.292 + +- Added `--marketplace ` to `claude plugin install`: adds the marketplace if needed, under the same policy checks as `claude plugin marketplace add`, then installs the plugin from it +- Added an `effort` parameter to the Agent tool, so Claude runs a sub-agent at the effort level you ask for +- Added `CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MS` environment variable to set a longer base delay for the backoff when retrying an overloaded (529) request +- Added `prompt.autocomplete`, an event a mod hooks to add its own rows to the prompt box's autocomplete list +- Added prompt caching to `$.model.complete` for mods: `prompt` and `system` take blocks of text, and `cache: true` on a block caches the request up to it +- Added workflow agents to the `agent.spawn` mod hook, with their run and index, so a mod can refuse them +- Fixed subagent definitions with `permissionMode: auto` entering auto mode when auto mode is unavailable (disabled by settings, circuit breaker, or a model that doesn't support it) +- Fixed sandboxed commands being able to read the staged file copies of `/ultrareview` uploads under `~/.claude/seed-admin` +- Fixed a managed sandbox read-deny path (and user ones beside it) that appears or re-points mid-session not dropping project grants inside it or ending credential injection from files it covers +- Fixed a notebook or PDF read on macOS and Windows being able to return a file outside what was approved, through a link swapped in mid-read +- Fixed a tampered on-disk cache of server-managed settings being able to switch off or unseat the built-in policy plugin while the settings fetch failed +- Fixed `rm -rf` on the 8.3 short name or another alternate Windows spelling of the home folder or a drive not being treated as removing it +- Security: Fixed PreToolUse hook approvals and auto mode bypassing the permission prompt for file reads from network (UNC) paths +- Fixed a skill's or slash command's `allowed-tools` rule coming back in a later turn when you leave auto mode or plan mode partway through that turn +- Fixed `NO_PROXY` being ignored for Claude Code's own API requests (sign-in, policy, feedback, artifacts) when `HTTPS_PROXY` is set +- Fixed an MCP tool with a name longer than 128 characters making every request fail; that tool is now left out and an MCP error names it +- Fixed `claude plugin` commands such as `marketplace add` and `install` running before an organization's managed settings had loaded on a first run +- Fixed one-shot `claude -p` and Agent SDK runs stopping a background command 5 seconds after the final result, and one-shot `claude -p` runs dropping a scheduled wakeup; both are now waited for +- Fixed plan mode not being restored when resuming a session from the `claude --resume` session picker or with `/resume` +- Fixed saved scheduled tasks created after `/resume`, `/branch` or `/clear` never firing, and saved tasks ignoring later creates and deletes after two writes to the tasks file milliseconds apart +- Fixed a background session's `/loop` silently stopping when the session's process restarted (for example after a crash), because its pending wakeup was lost +- Fixed Grep and Glob reporting no matches when the file or folder they were given could not be read; Claude now retries once or tells you +- Fixed the Read tool returning only the first entry, with no error, when a PDF's `pages` was a list such as "6,9,15"; it now returns an error saying to read each page or range separately +- Fixed @-mentioned text files over 256KB being left out silently: Claude is now told the file's size and to read it in portions +- Fixed the usage limit alert repeating once per background agent when agents failed on a limit that had already stopped the main conversation +- Fixed Remote Control viewers seeing an empty subagent pane for background subagents in sessions hosted by the desktop app or an IDE +- Fixed cross-session delivery notices showing two sessions with similar names as one recipient, and the expiry notice blaming the desktop app when a terminal session let the message lapse +- Fixed Send now in the desktop app ending the subagent a turn was waiting on when another message was already queued +- Fixed `/bug`, `/share` and `/feedback ` starting over after Ctrl+O or Ctrl+Z while a report was being sent, and closing as cancelled after it had been sent +- Fixed `/remote-env` replacing your saved default environment when you pressed Enter right away: the list now opens on your default, and no row has a check mark when no default is in effect +- Fixed some pasted text reaching Claude as typed text when several pastes overlapped in one prompt +- Fixed vim mode leaving the cursor past the end of a line, j/k losing their column on shorter lines, and `f`/`t`/`F`/`T`/`;`/`,` jumping to, or deleting up to, a match on another line of the prompt +- Fixed `/add-dir` path box letting Shift+Enter or a paste add a line break, and treating fast-typed "tab", "up" or "down" as those keys +- Fixed fast typing, input-method text and decomposed accents being dropped while a prompt footer row was selected, and `!` leaving the row selected +- Fixed fullscreen mode sending a full-screen clear on every window resize and Ctrl+L when iTerm2 is detected, which may be what filled iTerm2's scrollback with stale pages +- Fixed a spurious "could not be examined" note for @-words that name no file when a Read deny rule is set and the working directory is under a symlink +- Fixed "instruction file not loaded" lines going stale or missing after `/cd` or a permission change, and added a transcript line when a nested one isn't loaded +- Fixed a compaction summary that repeated `/name` letting Claude invoke a skill that is reserved for the user +- Fixed Write, Edit, NotebookEdit and LSP rows, and single Read, Grep and Glob rows, hiding why a mod denied the call: the row now shows the reason +- Fixed a cloud session showing a turn that never ended when its worker was stopped just as the turn finished +- Fixed cloud sessions with a large transcript sometimes asking for a permission again after it was approved +- Fixed scheduled tasks and other queued notifications being lost in cloud sessions when a message was retried or edited while Claude was reading them +- Fixed cloud sessions forgetting the thinking setting chosen in the client when the session's container restarted +- Fixed Cowork cloud sessions saying a proxy blocked artifacts when Anthropic couldn't confirm the organization's settings +- Fixed plugins whose hooks module makes many `$.state` calls through one const taking minutes to load or validate +- Fixed `claude plugin validate` listing a matcher or state value for a hooks module that the engine reads from elsewhere +- Fixed `claude plugin validate` listing a `$.state` value read through a top-level `var` that was declared again or reassigned; such a module is now refused +- Fixed a plugin's served `$` method restarting the hook origin, which could run a guard hook with a `.catch` above it again without end +- Fixed plugin interface calls made while the plugin hooks worker restarts running without the hooks other plugins put on them +- Fixed a mod's `config.set`, `state.set`, `env.set` or `agent.spawn` hook that denies after calling `next(e)` being answered as a refusal: the hook is now reported as failed, by name +- Fixed `/theme`, the `/config` Theme menu and the first-run theme step saving a theme before a plugin's `config.set` hook was asked +- Fixed a plugin's `tool.check` hook answering allow running a tool that requires your answer (a question, a plan approval) without showing its dialog +- Fixed a mod's start-up prompt, command or subagent being queued a second time when the hooks worker was replaced +- Fixed a mod's hook that called `next(e)` and then failed while the turn was interrupted letting the call through; the call is now rejected +- Fixed a plugin's prompt drop or setting deny being ignored when its reason was longer than 4,096 characters +- Fixed an organization's plugin being unloaded on its own reload, or after another plugin crashed, when it returned a `$` name that a user-installed mod had added; the mod is now unloaded instead +- Fixed tool calls made while the plugin hooks worker restarts being answered without the plugins' permission hooks +- Fixed plugin `tool.call` hooks seeing some tool calls before misnamed parameters were repaired; a hook now sees the arguments the tool will run with +- Fixed a mod's guard hook with a `.catch` being skipped silently for calls another mod's hook makes beneath the guard's own `$` call; its `.catch` is now asked +- Improved startup of `claude -p` and SDK sessions: the first turn no longer waits for HTTP and SSE MCP servers to answer `resources/list` +- Improved rendering speed of long bulleted or numbered replies: they stream, resize and re-open in the transcript (ctrl+o) much faster +- Improved Ctrl+C draft recovery: a cleared prompt now stays reachable with Up after a slash command or a sent message +- Improved hook output handling: `` tags written in a hook's output are escaped before they reach Claude +- Improved tool input handling: Grep accepts `file_path` for `path`, and Write, WebFetch and Read ignore a few stray parameters instead of failing the call +- Improved the steps shown when a marketplace declared in a settings file has a name that looks like an official Anthropic marketplace +- Improved sandbox auto-allow: with strict sandbox mode set in user, managed or --settings settings, an interpreter command with an env var prefix like `FOO=bar python3 app.py` runs unprompted +- Improved the Artifact tool's listing: Claude now sees how many published artifacts you have and can list up to 200 at once instead of 50 +- Improved cloud sessions after a restart: Claude is now told which stopped background agents it can resume by id +- Improved the Claude in Chrome message in claude.ai cloud sessions when the browser can't be reached: Claude is now told it may continue with alternatives if the user prefers +- Improved the /focus tip: it now invites you to try focus view mid-turn and shows how to switch back +- Improved startup with local (stdio) MCP servers that ignore the newer protocol check: after one slow connect they are remembered for 7 days and connected the older way without the wait +- Changed local (stdio) MCP server connections to negotiate protocol version 2026-07-28 by default on every install, including Bedrock, Vertex and Foundry; `MCP_PROTOCOL_NEGOTIATION=legacy` opts out +- Changed `claude plugin test`: a failed `expect` inside a hook the test registered, or a stub answer the engine refuses, now fails the test instead of passing silently +- Changed usage limit messages to write claude.ai settings links with https:// so terminals and apps can make them clickable +- Changed scheduled and Run now routine runs to publish a new artifact only you can see without asking for approval; artifacts that request connectors or other access still ask +- Changed agent names to allow at most 256 characters: a longer one is rejected, and a skill's or a plugin file's `name` longer than that is ignored +- [Cloud sessions] Fixed routine runs occasionally staying listed as running for hours after they had finished +- [Cloud sessions] Fixed editing or duplicating a routine turning off its push notifications when the routine had no saved notification setting +- [Cloud sessions] Fixed SVG, HEIC, TIFF and other less common image files failing to attach; they now attach as regular files +- [Cloud sessions] Fixed approval prompts offering "Always allow" for connector tools that an organization set to require approval; the choice had no effect +- [Remote Control] Fixed the first message of a new Remote Control session started from claude.ai/code accepting only images; it now accepts PDFs and other files like later messages +- [Claude Tag] Added an Edit button to the Allowed domains card on a channel's Configure page, so Enterprise admins can open the access bundle that sets the channel's domains +- [Claude Tag] Fixed replies sent in a Slack thread while Claude was still on its first request there being held until that request finished, or missed when sent seconds apart +- [Claude Tag] Fixed Slack threads woken only by GitHub pull request activity or a routine staying on their original model after an admin changed the channel or workspace default model +- [Claude Tag] Fixed Claude sometimes posting a spend limit notice in Slack when the real cause was that your organization had run out of usage credits +- [Claude Tag] Fixed a session hanging until interrupted when a permission prompt that can't be answered from Slack was denied automatically +- [Claude Tag] Improved `@Claude !status` in a channel to say when Claude has stopped reading its untagged messages, why, and that an @-mention starts it reading again +- [Claude Tag] Changed the first message of a Slack thread continued with `!fork` to a card showing where it came from, the request, and who asked, with a link to the original thread +- [Claude Tag] Changed the organization-wide and default spend limit boxes on Claude Tag's spend limits page in admin settings to save only when you press Save or Enter, not when you click away +- [Code Review] Added the period's total with its change from the previous period, and a breakdown by repository, to the PRs reviewed chart in Code Review analytics +- [Code Review] Fixed a queued review failing when the pull request moved to a new base branch and the old one was deleted; the commit is now re-queued for review +- [Code Review] Fixed reviews ignoring a CLAUDE.md's rules when the pull request edits that file; reviews now use its version from the base branch + +## 2.1.291 + +- Fixed a regression in 2.1.290 where cloud sessions could drop answers to permission prompts +- Fixed a regression in 2.1.288 where the last messages of a session could be lost when quitting + +## 2.1.290 + +- Added `serverToolUses` to the result of a mod's `turn.step` hook: the tool calls the API ran itself (the advisor), each with its id, name, input, start and end +- Added `agentId` to the `tool.check` event of plugin hooks, so a hook can tell a subagent's permission check from the main session's +- Added `ceiling` to the question and verdict a mod's `tool.check` hook reads, naming the approval an organization requires for a tool +- Added `ThemeKey` and `Color` types to the plugin hooks typings, so an editor lists the theme colors a mod's drawing can name +- Added to `claude plugin validate`: each hook a mod registers at a gating site is listed with whether it has a `.catch` (`gatingHooks` under `--json`) +- Added a Deny button to the Claude apps gateway's sign-in approval page: it ends the pending sign-in, so the waiting terminal stops within seconds +- Added `claude attach ` and `claude logs `: part of a session name works in place of the id +- Added `/claude-api managed-agents-onboard ` to set up the Managed Agents pattern a page describes as `ant apply` files +- Added `/claude-api managed-agents-onboard ` to build a Console quickstart template, such as `deep-researcher`, with the `ant` CLI +- Added a warning when a managed settings file is a link to a file outside the managed settings folder +- Added a /status and doctor warning when managed settings ignore user-configured sandbox allowRead paths or allowed domains +- Fixed requests failing behind proxies and gateways that reject one of Claude Code's beta headers with a status other than 400, or together with a second beta +- Fixed long sessions with hundreds of images getting stuck on "Request rejected as unprocessable by the model" errors +- Fixed a turn ending at once when the API's output content filter stopped a reply while Claude was still thinking; the request is now retried once before the error is shown +- Fixed resumed subagents and teammates losing their earlier thinking and prompt cache after receiving a message mid-run +- Fixed WebFetch silently dropping page text past 100,000 characters; it now says how much was unread and takes an `offset` to read on +- Fixed a crash ("Maximum call stack size exceeded") when a response nested lists or quotes thousands of levels deep +- Fixed `/rewind` not listing a prompt sent while Claude was still working +- Fixed scheduled tasks (`/loop` with an interval, reminders) silently not coming back on resume once the conversation was compacted; covers compactions made from this version on +- Fixed scheduled tasks set in the foreground never firing after a ← or `/background` hand-off, and recurring ones firing an extra run on every resume, respawn or fork +- Fixed headless `--json-schema` runs exiting non-zero with `is_error: true` on a `success` result when the connection dropped after the structured output was already delivered +- Fixed plan mode letting the auto mode classifier approve non-read-only connector tools that carry a server-pushed ask policy +- Fixed a project `CLAUDE.md`, rule or `AGENTS.md` symlinked outside the working directories loading under `permissions.blockReadsOutsideWorkingDirectories` or a `Read` deny rule +- Fixed URL allow and deny patterns with a wildcard inside an `xn--` host label matching differently from one process to the next +- Fixed an MCP server provided by your organization being relisted as your own after signing in or reconnecting, including from a late result in headless and SDK sessions +- Fixed `/ultrareview` dropping uncommitted changes without a warning on Windows when `git stash create` failed, and refusing them after a `git add -N` file was deleted or moved +- Fixed the `plansDirectory` setting's project-root check for paths that contain a backslash on macOS and Linux +- Fixed replies in very long Remote Control and cloud sessions that could appear a block at a time instead of streaming in +- Fixed the background daemon's log passing terminal control characters to the screen under `claude daemon run` and `claude daemon logs`; they now show as `\uXXXX` escapes +- Self-hosted runner: Fixed a crafted, very long line of a session's error output freezing the runner for several seconds +- Fixed a plugin hook with a `.catch` being unloaded, and its `.catch` skipped, when the hook kept the hooks worker busy on a prompt or tool call +- Fixed a mod's `turn.step` result listing a tool call that a mid-response model fallback had discarded +- Fixed a Cowork cloud session's reply sometimes never finishing when its container restarted just after Claude sent a message or a file +- Fixed `claude plugin validate` and plugin loading refusing a hooks module that destructures an option named like one of its top-level functions +- Fixed `/ultrareview` failing to upload uncommitted changes when `core.safecrlf=true` is set in git's configuration +- Fixed the effort level changing when a flagged message is retried on a fallback model that has a different level saved in settings +- Windows: Fixed multi-line `!` shell blocks in skills and commands failing when the file is saved with CRLF line endings +- Fixed Claude Code hanging until killed when a `/permissions` tab was clicked while searching in fullscreen mode +- Fixed conversation compaction sometimes failing with a "null is not an object" error +- Fixed plugin hooks reading an empty `answer` on `turn.complete` for a subagent that hands its report back in auto mode +- Fixed a mod being unloaded without a message when a refresh followed its failed reload; its failure line now says the version loaded before is unloaded +- Fixed a mod's `prompt.submit` hook that drops a prompt after calling `next(e)` being ignored silently: the hook is now reported as failed, by name +- Fixed a mod's pane or band being redrawn without end when it followed its end over a tree that changed height at every drawing +- Fixed an image read on macOS and Windows being able to return a file outside what was approved, through a link swapped in mid-read +- Fixed a case where a user-installed mod could get an organization's plugin unloaded; the mod is now the one unloaded +- Fixed `disableClaudeAiConnectors` and `allowedMcpServers` URL rules not being applied to some MCP entries declared in `.mcp.json`, plugins or agents +- Fixed a mod's inline pane being redrawn without end when its tree changed height at every drawing +- Fixed an `@`-mention under the read block or `--restricted` being able to read a file outside the working directories through a link changed mid-read +- Fixed Esc in the agents view confirming "Press enter again to restart this session — it isn't responding"; Esc now just reopens the session +- Fixed agent view losing a background session's `/loop` run count, countdown and live status line after the session enters a worktree that it creates +- Fixed `claude agents` sessions in manual permission mode asking for approval to read an image pasted into a reply or a new agent's prompt +- Fixed a deny or ask rule missing a command or path whose name came from a variable set as a prefix on `declare`, `typeset`, `export` or `readonly` +- Fixed Read deny rules not applying to image paths pasted or dragged into the prompt, or to file names listed for an @-mentioned folder +- Fixed a case where a user-installed mod could make an organization's guard skip its check; such a mod is now unloaded +- Fixed plugin hooks stalling each redraw when a mod draws a long multi-line text holding non-Latin characters +- Fixed repeated Ctrl+X in the agents view deleting the whole next section after the bottom session of a section was deleted +- Fixed You should know writing its notes in English regardless of the `language` setting +- Fixed a freeze after sending some very long messages +- Fixed a slowdown when expanding the transcript (ctrl+o) or resizing over large tool output that contains non-ASCII characters such as arrows, dashes or box-drawing +- Fixed `claude respawn` re-sending an earlier message to a backgrounded session that has no saved transcript instead of starting it with an empty conversation +- Fixed Esc after an `n:` or Ctrl+F search in the agents view moving focus to a section header, where Ctrl+X twice would delete every session in the section +- Fixed `claude agents` saving a slash command it could not deliver to a stopped session and then running it by itself the next time that session restarted +- Fixed `/ultrareview` uploading uncommitted changes unfiltered for files under a git filter driver named `unset` or `unspecified`; the upload now stops and asks you to rename the driver +- Fixed auto mode denials suggesting a permission rule that would skip the classifier for a whole tool or that Claude Code would ignore +- Fixed `claude --teleport` and `/teleport` deleting the files in a folder that had replaced a tracked file of the same name when you chose to stash: the stash is now refused, and says why +- Fixed Esc confirming agent view's "Press enter again to restart this session fresh" prompt +- Fixed agent view's `/loop` run count freezing and its countdown disappearing after `/clear`; the count now restarts with the new conversation +- Fixed `--channels` permission relay: a reply ID that repeats within a session is now ignored instead of approving a different prompt +- Fixed `/chrome` "Reconnect extension" not restoring browser tools after a failed Chrome connection, and added an explanation when it can't (anthropics/claude-code#98135) +- Fixed mods staying off for people who reach Claude through a gateway (`ANTHROPIC_BASE_URL` with `ANTHROPIC_AUTH_TOKEN`) and have no Anthropic account +- Fixed replies sent from `claude agents` just after a background session crashed being refused after 2 seconds: they are now retried for up to 12 seconds while the session restarts +- Fixed slash commands and answers to a multiple-choice question that `claude agents` could not deliver to a running session being saved and sent by themselves the next time it was restarted +- Fixed sandboxed commands that pipe a heredoc into another command (`cat <` in the terminal +- Fixed a marketplace named after another GitHub marketplace's download folder stopping that marketplace from downloading +- Fixed automatic compaction giving up with "Prompt is too long" when a Mac went to sleep while it was running +- Fixed the rewind menu (Esc Esc / `/rewind`) freezing for hundreds of milliseconds per keypress when the conversation contains a very large pasted stack trace or source file +- Fixed a subdirectory's AGENTS.md not being attached when a file under it is @-mentioned +- Fixed self-hosted runner sessions resumed after a stopped runner failing with "missing but already registered worktree" when the sessions folder is a relative symlink +- Fixed a freeze when the secret scan or a permission prompt met long token-like text +- Fixed Bash permission checks not applying Read deny rules or the outside-directory read block to a wildcard in some option values of read-only commands +- Fixed `CLAUDE_CODE_USER_DIALOG_TIMEOUT_MS=5m` being read as 5 ms and cancelling remote dialogs at once; values with a unit suffix now fall back to `dialogExpiry` +- Fixed a stall when an MCP server's tool listing contains very long runs of combining characters +- Fixed two pastes that overlap in one prompt being sent to the model partly as typed text instead of as one pasted block +- Fixed Claude in Chrome's browser picker showing a message meant for Claude when the chosen browser is no longer connected, and the VS Code dialog's list going stale after a switch +- Fixed background subagents losing write and Bash access in their worktree after the main session enters or exits a different worktree +- Fixed background commands, the agents view and daemon workers sending telemetry and a feature-flag request to Anthropic behind a Claude apps gateway when no managed settings on the machine force gateway login +- Fixed `--restricted` (and `CLAUDE_CODE_RESTRICTED=1`) sessions opening the cross-session messaging socket +- Fixed sessions moved to the background while idle reopening as "no saved transcript" after a restart or idle cleanup; they now resume their conversation +- Fixed background workers honoring `--allow-dangerously-skip-permissions` on respawn without the bypass-permissions disclaimer having been accepted +- Fixed Claude replying in an endless loop when a plugin's async Stop hook passes an unquoted script path under a folder with a space, such as Application Support +- Fixed a freeze of several seconds when secret masking met very long unbroken text +- Fixed some permission rules and safety checks not being applied to a tool call after a PreToolUse hook rewrote its input +- Fixed first launch asking to pick a login method again after `claude auth login` or with a credentials file already in the config directory +- Fixed file names containing line breaks being displayed incorrectly in file tool errors and permission prompts +- Fixed a large paste expanded in place being sent to the model as typed text after the next keystroke when it held accents stored as separate characters, as macOS file names do +- Fixed macOS `/login` reporting success when the keychain refused the new login and kept an old one it could not remove +- Fixed SDK hosts using `--include-partial-messages` seeing a reply stay open after the turn ended when its stream was cut, interrupted or fell back to non-streaming +- Fixed sandboxed Bash commands on Linux running `ConfigChange` hooks and reloading settings mid-command when `.claude/settings.json` or `.claude/settings.local.json` does not exist +- Fixed errors reading "Premature close" instead of naming the missing program when a tool Claude Code runs, such as git or gh, is not installed (macOS, Linux) +- Fixed `/loop` and other recurring session-only scheduled tasks running an extra time after a sandboxed Bash command on Linux or after `.claude/scheduled_tasks.json` was deleted +- Fixed edits to the file a symlinked settings file points at running without the settings-file permission question +- Improved MCP startup behind a network proxy: a server the proxy blocks (HTTP 403) is no longer retried three times +- Improved permission prompts from background agents to show the Ctrl+X Ctrl+K shortcut that stops all background agents +- Improved the built-in `plugin-authoring` skill: Claude now gives the one command another person runs to install a mod you made, and writes it in a README's install section +- Improved the reply to `/plugin` in the desktop app's Code tab: it now says where to install and manage plugins there +- Improved the Bash changed-files view: when a chained command includes git merge, pull or checkout, it lists the files without full diffs +- Improved the Claude apps gateway's log when an upstream's cloud credentials or connection fail: the warning now ends with the underlying cause +- Improved the error shown when a cloud session is started without a claude.ai sign-in: it now names `claude auth login` and /login and no longer blames API-key authentication +- Improved the Read tool's message for binary files: it now points Claude to a skill or a shell command that can read the format +- Improved the error shown when a git config file stops the `/ultrareview` upload: it is about half as long and says what kind of file is the problem +- Improved the errors shown when the `/ultrareview` upload refuses a checkout: each known cause now has its own message, with a way to fix it +- Improved the Claude apps gateway to log a warning during the last 30 days before the certificate it presents to the identity provider expires +- Improved Claude in Chrome: a `browser_batch` call now gets 90 seconds, up from 60, before it is reported as timed out +- Improved the Claude apps gateway's browser sign-in pages: brand fonts, centered layout, and dark mode +- Improved responsiveness while resuming large sessions: timers, input and rendering keep running while the transcript loads +- Improved the / and @ suggestion lists: the selected row now starts with a ❯ pointer, so you can see it without color +- Changed `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` to also skip the startup connection warm-up +- Changed Claude in Chrome so that a project's settings files can no longer turn it on; use `--chrome`, `/chrome` or your user settings +- Changed the Bash tool to ask for permission before running `pyright`, which is no longer treated as a read-only command +- Changed what a mod's `$.process.spawn` rejects with when another mod denies it after the child ran: it now says the call ran and a plugin withheld its result +- Changed the background daemon's log to write a multi-line message as one JSON-quoted line +- Changed skills and custom commands to refuse a `!` shell command that contains raw control characters other than tab and newline, with a message that shows where they are +- Changed `/artifacts`: opening an artifact in your browser now closes the list +- Changed Bash permission checks so that more forms of the `ps` command ask for approval instead of running without asking +- Changed plugin hooks so long text is clipped and logged instead of being refused or dropped silently +- Changed background sessions whose scheduled task is gone: they now move to Completed about 20 seconds later and can be updated or shut down when idle +- Changed the "Press ← again" confirm on a just-cleared prompt: a second ← no longer has to wait a second before it switches, and holding ← down now switches too +- Changed the errors shown when the `/ultrareview` upload fails at a git step: they name the step and what to try, and no longer repeat git's own error text +- Changed `/code-review` at medium effort to also report cleanup and CLAUDE.md conventions findings on models without tuned review settings, including Opus 5.5 and Sonnet 5.5 +- Changed an in-process teammate's `agent_id` in Agent results to its agent ID (its `name@team` address stays in `teammate_id`); TeammateIdle hooks no longer fire from its subagents or forks +- Changed background sessions waiting on a scheduled wakeup (`/loop`): they are now left running through updates and low memory, where being restarted or shut down could silently lose the wakeup +- Changed `/model`, `/effort` and `/rename` sent from `claude agents` to a busy background session to apply right away, without a confirmation, instead of when the turn ends +- Changed the Claude apps gateway's minimum supported PostgreSQL version from 14 to 11 +- Changed the interactive session's WebSearch budget to refill over time (100 calls/hour; `CLAUDE_CODE_WEB_SEARCH_REFILLS_PER_HOUR` sets the rate, 0 turns it off) instead of ending after 200 calls +- Changed `CLAUDE_CODE_DISABLE_ATTACHMENTS` so a repository's `.claude/settings.json` or `.claude/settings.local.json` can no longer set it; shell, user and managed settings still can +- Changed `claude plugin update` on a plugin loaded from a directory to print just its reason, without the "Failed to update plugin" prefix, as for built-in plugins +- Changed the built-in `gh api` in cloud sessions: a host other than github.com set in `GH_HOST` or `GH_REPO` is now refused (use `--hostname` or a full URL), and stderr notes requests to other hosts +- Changed the `claude-api` skill's Managed Agents examples to turn off the web tools unless the agent needs them and to use the `auto` permission policy +- Self-hosted runners: Changed `claude --environment ` to create its session through the current Sessions API; printed and JSON session ids keep their session_… form +- [VSCode] Added a screen reader announcement, "Message queued.", when you send a message while Claude is working +- [VSCode] Added a way to review and run a plugin marketplace's install or update command from the Manage plugins dialog +- [VSCode] Fixed a blank chat you never typed into keeping a background Claude process running after you open a saved conversation in its place +- [VSCode] Fixed settings dialogs blaming a timeout when Claude Code stopped unexpectedly during a save +- [VSCode] Fixed the branch switch dialog offering to switch when it could not check for uncommitted changes +- [VSCode] Fixed a permission prompt that arrived behind an open dialog taking keyboard focus, so a key pressed in the dialog could answer it +- [VSCode] Fixed sign-in and new sessions giving no clear reason when Claude Code cannot find or start its program +- [VSCode] Fixed the agent map showing a nested sub-agent with "Tool calls (0)" and placing the agents it starts under the main agent +- [VSCode] Improved Continue After Reload: tabs reopened after VS Code restarts its extensions now also finish a step the restart interrupted +- [VSCode] Improved file pills in messages: hovering one now shows the file's path from the project folder, so same-named files can be told apart +- [VSCode] Changed message timestamps to show by default (turn them off with the Claude Code: Show Message Timestamps setting) +- [Cloud sessions] Fixed turning off prompt suggestions through a cloud environment's environment variables having no effect in new cloud sessions +- [Cloud sessions] Fixed the working indicator in a cloud session spinning on for several seconds after Claude's reply had finished; it now stops with the reply +- [Cloud sessions] Fixed History on a never-run routine's page still saying "No runs yet" after you pressed Run now; it now shows the new run +- [Cloud sessions] Fixed an unarchived cloud session looking as if Claude were still working until you sent another message +- [Remote Control] Fixed a computer that just started Remote Control taking up to a minute to appear in the Remote Control menu of a new session; it now appears within seconds +- [Claude Tag] Added fast mode in Slack: mention Claude with `!fast` to switch a thread to fast mode, moving it to Opus if needed, and `!fast off` to switch back; replies show (fast) while it's on +- [Claude Tag] Added the optional Path prefixes field when creating a custom connection in an access bundle, so its allow rule can cover only those paths instead of the whole host +- [Claude Tag] Fixed members with the Claude Tag Admin permission getting "Couldn't load memory files" on the Activity page's Memory tab; they can now read workspace and channel memory +- [Claude Tag] Fixed a workspace guest's Confirm on a Claude settings card in Slack removing its buttons for everyone; only the guest sees the refusal, and members can still confirm or cancel +- [Claude Tag] Fixed scheduled routines in Slack channels running on a model other than the channel's default; each run that starts a new session now uses the current default model +- [Claude Tag] Fixed GitHub repositories in an access bundle attached by a channel-name rule being refused in the channels the rule covers; Claude can now add, list and clone them there +- [Claude Tag] Improved Claude's notice in your direct messages when your own Claude plan's usage limit is reached: it shows within seconds and says when the limit resets +- [Claude Tag] Improved the earlier Claude in Slack app's reply when it can't start a session: it now says what failed and who can fix it, in full only once per thread +- [Claude Tag] Changed the channel instructions limit to 8,192 characters instead of bytes, so non-English text gets the same room, and added a character count beside Save on the Configure page +- [Code Review] Fixed blocking review comments sometimes opening with a "nit" label that contradicted their severity +- [Code Review] Fixed tips to comment "@claude review" being posted on fork and Manual-mode pull requests in organizations that have turned Code Review off + +## 2.1.289 + +- Fixed a deny or ask rule on a nested part of a compound shell command not holding over a user-installed mod's approval on managed machines +- Fixed the terminal freezing on short code blocks with many unclosed `