spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

Merge remote-tracking branch 'origin/main' into claude/diff-pane-empty-auto-openBrowse files
Conflicts resolved:

mods/diff/hooks/register.ts: main's gates (the main loop's edit only,
checkpointing on, an open left waiting withdrawn) now sit before this
branch's fetch that decides whether to open. openPane seeds the model from
the read it is handed, so the pane lists as it opens, and loads hunks only
once the engine has placed it; a withdrawn open ends the attempt and the
next edit asks again.

mods/diff/tests/register.test.ts: both sides' tests kept. main's "an open
left waiting is withdrawn" now expects the one deciding read before the
open, and still no hunks fetched for a pane no one sees.

mods/diff/README.md: both descriptions combined.
Claude authored on Sep 18, 2026 parents 2bdd1fd + 2287e5d · commit a2468f816953
93 changed files+2305 -222compared with 2bdd1fd
MCHANGELOG.md
Mfeed.xml
Mmods/README.md
Amods/agents-md/.claude-plugin/plugin.json
Amods/agents-md/README.md
Amods/agents-md/hooks/files/chain-root-of.ts
Amods/agents-md/hooks/files/dropped-kinds.ts
Amods/agents-md/hooks/files/files-of.ts
Amods/agents-md/hooks/files/index.ts
Amods/agents-md/hooks/files/insertion-index.ts
Amods/agents-md/hooks/files/is-claude-file-on-walk.ts
Amods/agents-md/hooks/files/is-kept-without-instructions.ts
Amods/agents-md/hooks/files/is-project-own.ts
Amods/agents-md/hooks/files/project-dir-of.ts
Amods/agents-md/hooks/files/unseen-files.ts
Amods/agents-md/hooks/files/with-project-files.ts
Amods/agents-md/hooks/frames/absolute-of.ts
Amods/agents-md/hooks/frames/index.ts
Amods/agents-md/hooks/frames/is-below.ts
Amods/agents-md/hooks/frames/is-file-at.ts
Amods/agents-md/hooks/frames/nested-frame.ts
Amods/agents-md/hooks/frames/normal-spelling-of.ts
Amods/agents-md/hooks/frames/outside-claude-dirs.ts
Amods/agents-md/hooks/hooks.json
Amods/agents-md/hooks/index.ts
Amods/agents-md/hooks/modes/default-mode.ts
Amods/agents-md/hooks/modes/index.ts
Amods/agents-md/hooks/modes/legacy-mode-of.ts
Amods/agents-md/hooks/modes/mode-of.ts
Amods/agents-md/hooks/modes/modes.ts
Amods/agents-md/hooks/modes/types/index.ts
Amods/agents-md/hooks/modes/types/mode/index.ts
Amods/agents-md/hooks/modes/types/mode/mode.ts
Amods/agents-md/hooks/names/agents-names.ts
Amods/agents-md/hooks/names/claude-names.ts
Amods/agents-md/hooks/names/index.ts
Amods/agents-md/hooks/names/main-loop.ts
Amods/agents-md/hooks/register.ts
Amods/agents-md/hooks/switches/index.ts
Amods/agents-md/hooks/switches/is-switched-on.ts
Amods/agents-md/hooks/telemetry/feature-name.ts
Amods/agents-md/hooks/telemetry/index.ts
Amods/agents-md/hooks/telemetry/load-counts-of.ts
Amods/agents-md/hooks/telemetry/load-event.ts
Amods/agents-md/hooks/telemetry/load-mark-of.ts
Amods/agents-md/hooks/telemetry/load-row-of.ts
Amods/agents-md/hooks/telemetry/mode-choice-of.ts
Amods/agents-md/hooks/telemetry/mode-event.ts
Amods/agents-md/hooks/telemetry/mode-row-of.ts
Amods/agents-md/hooks/telemetry/nested-event.ts
Amods/agents-md/hooks/telemetry/nested-row-of.ts
Amods/agents-md/hooks/telemetry/quietly.ts
Amods/agents-md/hooks/telemetry/types/index.ts
Amods/agents-md/hooks/telemetry/types/load-counts/index.ts
Amods/agents-md/hooks/telemetry/types/load-counts/load-counts.ts
Amods/agents-md/hooks/telemetry/types/telemetry-mark/index.ts
Amods/agents-md/hooks/telemetry/types/telemetry-mark/telemetry-mark.ts
Amods/agents-md/hooks/telemetry/types/telemetry-prop/index.ts
Amods/agents-md/hooks/telemetry/types/telemetry-prop/telemetry-prop.ts
Amods/agents-md/hooks/telemetry/types/telemetry-row/index.ts
Amods/agents-md/hooks/telemetry/types/telemetry-row/telemetry-row.ts
Amods/agents-md/hooks/telemetry/walk-failed-reason.ts
Amods/agents-md/tests/fixtures/ancestor-of.ts
Amods/agents-md/tests/fixtures/index.ts
Amods/agents-md/tests/fixtures/project-of.ts
Amods/agents-md/tests/fixtures/recording.ts
Amods/agents-md/tests/fixtures/rows-of.ts
Amods/agents-md/tests/fixtures/session.ts
Amods/agents-md/tests/fixtures/started-of.ts
Amods/agents-md/tests/fixtures/types/index.ts
Amods/agents-md/tests/fixtures/types/row/index.ts
Amods/agents-md/tests/fixtures/types/row/row.ts
Amods/agents-md/tests/fixtures/types/started/index.ts
Amods/agents-md/tests/fixtures/types/started/started.ts
Amods/agents-md/tests/fixtures/types/toast-asked/index.ts
Amods/agents-md/tests/fixtures/types/toast-asked/toast-asked.ts
Amods/agents-md/tests/fixtures/unreadable-project-of.ts
Amods/agents-md/tests/register.test.ts
Mmods/diff/README.md
Mmods/diff/hooks/host/host.ts
Mmods/diff/hooks/index.ts
Amods/diff/hooks/is-checkpointing/index.ts
Amods/diff/hooks/is-checkpointing/is-checkpointing.ts
Mmods/diff/hooks/register.ts
Amods/diff/tests/fixtures/beneath/beneath.ts
Amods/diff/tests/fixtures/beneath/index.ts
Amods/diff/tests/fixtures/beneath/left-waiting.ts
Mmods/diff/tests/fixtures/in-repository.ts
Mmods/diff/tests/fixtures/in-slow-repository.ts
Mmods/diff/tests/fixtures/index.ts
Mmods/diff/tests/fixtures/old-files.ts
Mmods/diff/tests/register.test.ts
Mmods/types/claude-code.d.ts
Large file, diff not shown.
Mfeed.xml+317 -179
@@ -6,8 +6,324 @@
66 <author><name>Anthropic</name></author>
77 <link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md"/>
88 <link rel="self" type="application/atom+xml" href="https://raw.githubusercontent.com/anthropics/claude-code/main/feed.xml"/>
9- <updated>2026-09-15T20:22:55Z</updated>
9+ <updated>2026-09-18T18:06:17Z</updated>
1010 <entry>
11+ <id>https://github.com/anthropics/claude-code/releases/tag/v2.1.277</id>
12+ <title>Claude Code v2.1.277</title>
13+ <link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.277"/>
14+ <updated>2026-09-18T18:06:17Z</updated>
15+ <content type="html">&lt;p&gt;• Added AGENTS.md support: in a project with no CLAUDE.md, Claude Code reads AGENTS.md instead; change it under "Project instructions" in /config (not yet on Bedrock, Vertex or Foundry)&lt;/p&gt;
16+&lt;p&gt;• Added CLAUDE_GATEWAY_PROXY_IS_EGRESS_BOUNDARY=1 for Claude apps gateways whose only egress is a forward proxy: every outbound request hands the proxy the hostname instead of resolving it locally&lt;/p&gt;
17+&lt;p&gt;• Added an optional headers: map on Claude apps gateway upstreams, to send static headers to a proxy you run in front of a provider&lt;/p&gt;
18+&lt;p&gt;• Added a line saying a background task's update is waiting when it finishes while a panel such as /tasks is open&lt;/p&gt;
19+&lt;p&gt;• Fixed claude -p and Agent SDK sessions that could hang with no result after an internal error; they now report the error and exit with code 1&lt;/p&gt;
20+&lt;p&gt;• Fixed conversations failing every request with "text content blocks must be non-empty" when an earlier assistant turn held an empty text block beside other content, including after --resume&lt;/p&gt;
21+&lt;p&gt;• Fixed being unexpectedly logged out when an older Claude Code build (for example an IDE extension's bundled CLI) runs on the same machine as the current one&lt;/p&gt;
22+&lt;p&gt;• Fixed interactive start-up hanging or showing an error for ANTHROPIC_API_KEY users when ~/.claude.json holds a malformed customApiKeyResponses value&lt;/p&gt;
23+&lt;p&gt;• Fixed update checks erroring every 30 minutes, and claude update hanging when a minimum or maximum version is set, if a proxy returns an invalid version; a malformed minimumVersion is now ignored&lt;/p&gt;
24+&lt;p&gt;• Fixed claude update on winget- or apk-managed installs reporting "up to date" when the version lookup failed&lt;/p&gt;
25+&lt;p&gt;• Fixed claude plugin install sometimes failing and breaking the installed copy when reinstalling a plugin version that a session or another program was using; an unchanged copy is now left alone&lt;/p&gt;
26+&lt;p&gt;• Fixed Grep and Glob reporting no matches when the search could not start because the system was out of processes, memory or file handles; they now return an error saying so&lt;/p&gt;
27+&lt;p&gt;• Fixed the Write tool silently ending the turn as a declined permission when the target path is an existing directory; it now reports a clear error&lt;/p&gt;
28+&lt;p&gt;• Fixed the Edit tool treating an escaped backslash followed by uXXXX text as a \uXXXX escape, which could make an edit of a non-ASCII character rewrite an escaped backslash sequence instead&lt;/p&gt;
29+&lt;p&gt;• Fixed the Edit tool reporting "Invalid regular expression: regular expression too large" instead of "String not found in file" when a very large edit containing non-ASCII text did not match the file&lt;/p&gt;
30+&lt;p&gt;• Fixed a turn ending early with "Path contains null bytes" when a tool call's file path contained \u0000 written as an escape sequence; escaped control characters now stay as literal text&lt;/p&gt;
31+&lt;p&gt;• Fixed background sessions (claude --bg) exiting when a plugin's LSP server exited or closed its stdin&lt;/p&gt;
32+&lt;p&gt;• Fixed a crash ("Type error") when opening /mcp or /plugin manage with a malformed claudeAiMcpEverConnected value in ~/.claude.json&lt;/p&gt;
33+&lt;p&gt;• Fixed a crash at launch when ~/.claude.json holds a malformed theme value&lt;/p&gt;
34+&lt;p&gt;• Fixed a crash ("unrecoverable interface error") when the prompt held text containing terminal color codes, for example a prompt recalled from history or text loaded from the external editor&lt;/p&gt;
35+&lt;p&gt;• Fixed a crash when resuming a session whose saved history holds an assistant message stored as a plain string&lt;/p&gt;
36+&lt;p&gt;• Fixed sessions on slow or heavily loaded machines sometimes exiting with "Claude Code exited after an unrecoverable interface error" when the first spinner appeared&lt;/p&gt;
37+&lt;p&gt;• Fixed a rare case where the screen could stop updating for the rest of the session after an internal rendering error&lt;/p&gt;
38+&lt;p&gt;• Fixed a rare case on Windows where a turn could stop with an error such as "Out of memory" right after Claude replied, so that reply's tool calls never ran&lt;/p&gt;
39+&lt;p&gt;• Fixed sessions continued after /clear (restart, --continue, --resume) missing part of their first message when a SessionStart hook printed output, causing a full prompt-cache miss&lt;/p&gt;
40+&lt;p&gt;• Fixed messages from other agents (such as a subagent's SendMessage) that arrived mid-turn showing up below the "Ran N shell commands" row instead of where they arrived&lt;/p&gt;
41+&lt;p&gt;• Fixed the "copied" notice not appearing after drag-selecting text in the fullscreen /resume picker and other panels that cover the prompt area&lt;/p&gt;
42+&lt;p&gt;• Fixed $TMPDIR expanding empty in Bash commands that run outside the sandbox while sandboxing is enabled&lt;/p&gt;
43+&lt;p&gt;• Fixed WebFetch and WebSearch in Cowork cloud sessions not telling Claude why a request was refused, such as a used-up fetch budget or an admin policy&lt;/p&gt;
44+&lt;p&gt;• Fixed the Claude apps gateway's telemetry relay ignoring a collector hostname or domain listed in NO_PROXY when a proxy is set&lt;/p&gt;
45+&lt;p&gt;• Fixed one malformed strictKnownMarketplaces or blockedMarketplaces entry silently disabling the whole enterprise marketplace policy&lt;/p&gt;
46+&lt;p&gt;• Fixed failed auto-updates leaving large staged downloads behind in ~/.cache/claude/staging&lt;/p&gt;
47+&lt;p&gt;• Fixed /plugin not stripping terminal control characters from messages on the Installed tab, such as the error of a failed plugin update&lt;/p&gt;
48+&lt;p&gt;• Fixed /plugin → Installed and /skills crashing when a skill or legacy command is named like a built-in Object property such as constructor or toString&lt;/p&gt;
49+&lt;p&gt;• Fixed /plugin closing with no message when every install in a multi-select failed&lt;/p&gt;
50+&lt;p&gt;• Fixed uninstalled plugins reappearing as "failed to load" rows in /plugin Installed, and Remove not clearing such a row&lt;/p&gt;
51+&lt;p&gt;• Fixed plugins from the official marketplace being recorded without their commit in installed_plugins.json, and installed_plugins.json keeping the old commit after updating a pinned-commit plugin&lt;/p&gt;
52+&lt;p&gt;• Fixed plugin reload previews keeping every previewed copy of a plugin archive unpacked until exit, and overwriting the cached --plugin-url archive a reload falls back to when its download fails&lt;/p&gt;
53+&lt;p&gt;• Fixed Remote Control session bookkeeping failing when ~/.claude.json holds a malformed placeholder record&lt;/p&gt;
54+&lt;p&gt;• Fixed the error after a revoked claude.ai login blaming an expired Anthropic profile; it now leads with /login&lt;/p&gt;
55+&lt;p&gt;• Fixed typed or pasted text occasionally coming out scrambled in the claude agents dispatch input during key repeat or very fast input&lt;/p&gt;
56+&lt;p&gt;• Fixed a crash ("unrecoverable interface error") when resuming a session whose saved transcript contains a stop hook summary without a well-formed hook list&lt;/p&gt;
57+&lt;p&gt;• Fixed Enter on a selected agent panel row doing nothing when keybindings.json rebinds Enter in the Chat context, for example to chat:queueSubmit&lt;/p&gt;
58+&lt;p&gt;• Fixed PDF page reads on Windows failing when the working folder's path is long (about 120 characters or more)&lt;/p&gt;
59+&lt;p&gt;• Fixed a headless resume (claude -p --resume, the SDK, a VS Code extension window reload) starting the session's cost and usage totals at zero; headless sessions now save their totals at exit&lt;/p&gt;
60+&lt;p&gt;• Fixed project skills from the main repository not loading in --worktree sessions when .claude/skills is untracked&lt;/p&gt;
61+&lt;p&gt;• Fixed a sandbox.excludedCommands glob exempting an entire compound Bash command from the sandbox when only one part matched; every part must now match&lt;/p&gt;
62+&lt;p&gt;• Fixed resumed subagents and teammates re-rendering the MCP tool definitions they had loaded, which broke prompt caching for that agent&lt;/p&gt;
63+&lt;p&gt;• Fixed rate-limited artifact publishes telling Claude to stop retrying; Claude is now told nothing was published and when to send the same publish again&lt;/p&gt;
64+&lt;p&gt;• Fixed attachments recorded earlier in a conversation being re-rendered after a resume or relaunch, which dropped extended thinking and missed the prompt cache&lt;/p&gt;
65+&lt;p&gt;• Fixed Console sign-in showing only "Request failed with status code 400" when the server refuses to create an API key; it now shows the server's message&lt;/p&gt;
66+&lt;p&gt;• Fixed messages typed while Claude is still working sometimes being ignored by the model&lt;/p&gt;
67+&lt;p&gt;• Improved session start-up for SDK and headless (-p) use: the first turn no longer waits on the per-directory CLAUDE.md lookup&lt;/p&gt;
68+&lt;p&gt;• Improved the Claude apps gateway's loopback error messages to name CLAUDE_GATEWAY_ALLOW_LOOPBACK&lt;/p&gt;
69+&lt;p&gt;• Improved /plugin Installed: an MCP server listed apart from its plugin now shows which plugin it belongs to&lt;/p&gt;
70+&lt;p&gt;• Improved claude plugin install on an already-installed plugin: it now says when the marketplace offers a newer version and names the claude plugin update command&lt;/p&gt;
71+&lt;p&gt;• Improved the startup notice overflow line under the logo: it now reads "N more notices hidden" instead of "+N more · /status"&lt;/p&gt;
72+&lt;p&gt;• Improved prompt handling: invisible Unicode formatting and tag characters in a prompt are removed and the cleaned prompt is shown for review before it is sent&lt;/p&gt;
73+&lt;p&gt;• Improved /ultrareview when there's nothing to review: messages say which case you're in, offer a command that reviews your latest commit, and a new repository's first commit is reviewed in full&lt;/p&gt;
74+&lt;p&gt;• Improved artifact link handling so Claude reads claude.ai artifact links with the Artifact tool instead of WebFetch when that tool is available&lt;/p&gt;
75+&lt;p&gt;• Improved the dangerous-rm permission prompt to name the flagged rm command and suggest a ${VAR:?} guard, so headless runs can recover&lt;/p&gt;
76+&lt;p&gt;• Improved the Artifact tool's permission prompts: shorter sentences, pages and artifacts named by title or file name, and links listed after the text&lt;/p&gt;
77+&lt;p&gt;• Changed Fable to always appear in /model on the Anthropic API; it is greyed out only when your organization's settings disable it&lt;/p&gt;
78+&lt;p&gt;• Changed the Bash sandbox instructions on Bedrock, Vertex and Foundry to the first-party wording, which frames the sandbox as the boundary of what the task was given&lt;/p&gt;
79+&lt;p&gt;• Changed /ultrareview in non-interactive sessions to refuse when the repository has no base branch or shared history&lt;/p&gt;
80+&lt;p&gt;• Changed subagent results to reach the main agent under a header marking them as subagent output, with the result indented, so text in a subagent's result cannot pass as the session's own instructions&lt;/p&gt;
81+&lt;p&gt;• Changed workflow scripts' computed agent() prompts on Bedrock, Vertex and Foundry to reach the subagent framed as script-authored text, so the safety classifier does not read them as the user&lt;/p&gt;
82+&lt;p&gt;• Removed the background Haiku auto-title request from claude -p runs launched outside an SDK or IDE&lt;/p&gt;
83+&lt;p&gt;• Removed the deprecated TaskOutput tool; Claude reads a background task's output file with Read instead, and the taskOutputMaxChars setting and TASK_MAX_OUTPUT_LENGTH no longer have any effect&lt;/p&gt;
84+&lt;p&gt;• [VSCode] Added a Sign out row to the panel menu, with /logout in the typed command menu&lt;/p&gt;
85+&lt;p&gt;• [VSCode] Added background shells and other running tasks to the agent map, each with a Stop, and a typed /tasks that opens it&lt;/p&gt;
86+&lt;p&gt;• [VSCode] Added a Copy response button on responses and a typed /copy&lt;/p&gt;
87+&lt;p&gt;• [VSCode] Added a one-time notice when inactive sessions are archived automatically, and an "Unarchive all" action on the Archived sessions group&lt;/p&gt;
88+&lt;p&gt;• [VSCode] Added the session's cost and token usage to the Account &amp;amp; usage dialog and the session manager where plan limits do not apply (Vertex, Bedrock, Foundry, API key)&lt;/p&gt;
89+&lt;p&gt;• [VSCode] Fixed the "General config" menu row showing /config usage text instead of opening settings, and made typed /mcp, /hooks, /memory, /rewind and similar commands open their dialogs&lt;/p&gt;
90+&lt;p&gt;• [VSCode] Fixed the effort slider's level not persisting into later sessions on a model that already had a level saved with /effort&lt;/p&gt;
91+&lt;p&gt;• [VSCode] Fixed Auto missing from the mode picker for conversations opened in an already-used panel when the saved model setting is a differently-cased alias such as "Sonnet"&lt;/p&gt;
92+&lt;p&gt;• [VSCode] Fixed /fast not saving fast mode as the default, so it was lost when the extension relaunched Claude Code&lt;/p&gt;
93+&lt;p&gt;• [Claude Code on the web] Added Personal and Organization sections to the environment picker on Team and Enterprise plans, and admins can now share a personal environment with the organization&lt;/p&gt;
94+&lt;p&gt;• [Claude Code on the web] Changed organization environments to open as a read-only summary from the Code tab on Team and Enterprise plans, with editing under Admin settings → Cloud environments&lt;/p&gt;
95+&lt;p&gt;• [Claude Code on the web] Fixed a cloud environment saved with Custom network access and no domains silently reverting to Trusted; the dialog now asks for at least one domain&lt;/p&gt;
96+&lt;p&gt;• [Claude Code on the web] Changed the admin Claude Code setting labeled "Web" to "Cloud sessions" and removed the redundant read-only Mobile row beneath it&lt;/p&gt;
97+&lt;p&gt;• [Claude Tag] Fixed routines created in a Slack channel on an Enterprise Grid org-wide install failing to read other public channels in their workspace when they ran&lt;/p&gt;
98+&lt;p&gt;• [Claude Tag] Fixed the "Learn more" links on credential presets in Claude Tag access bundles to open each vendor's credential-setup page instead of a generic API reference&lt;/p&gt;
99+&lt;p&gt;• [Claude Tag] Changed the Pylon credential preset in Claude Tag access bundles so admins can point it at Pylon's EU host&lt;/p&gt;
100+&lt;p&gt;• [Claude Tag] Fixed Google Cloud credential forms in Claude Tag access bundles: a refused key file now says why, the website and scopes stay locked, and a rejected rotation keeps the pasted key&lt;/p&gt;
101+&lt;p&gt;• [Claude Tag] Fixed the network events log in Claude Tag admin settings showing no response status for requests through connections that use AWS signing, client certificates or a custom CA&lt;/p&gt;</content>
102+ </entry>
103+ <entry>
104+ <id>https://github.com/anthropics/claude-code/releases/tag/v2.1.276</id>
105+ <title>Claude Code v2.1.276</title>
106+ <link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.276"/>
107+ <updated>2026-09-18T02:12:04Z</updated>
108+ <content type="html">&lt;p&gt;• Fixed every request failing with 400 … Input tag 'advisor_20260301' when ANTHROPIC_BASE_URL points at a proxy or gateway (2.1.275 regression)&lt;/p&gt;</content>
109+ </entry>
110+ <entry>
111+ <id>https://github.com/anthropics/claude-code/releases/tag/v2.1.275</id>
112+ <title>Claude Code v2.1.275</title>
113+ <link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.275"/>
114+ <updated>2026-09-17T22:32:56Z</updated>
115+ <content type="html">&lt;p&gt;• Added the signed-in account to Claude apps gateway sign-in: when the gateway names it, you confirm it before the credential is saved, and /status shows it&lt;/p&gt;
116+&lt;p&gt;• Added a send-now key (ctrl+enter, or ctrl+x ctrl+s) that interrupts the current turn and sends all queued messages at once; sent and queued messages show in gray until the model receives them&lt;/p&gt;
117+&lt;p&gt;• Added a startup warning when a configured otelHeadersHelper fails, so sessions that silently export no telemetry are noticed&lt;/p&gt;
118+&lt;p&gt;• Added syncing of the skills and plugins enabled on your claude.ai account to terminal sessions signed in with it; opt out with syncClaudeAiSkills: false or syncClaudeAiPlugins: false&lt;/p&gt;
119+&lt;p&gt;• Added /plugin install &amp;lt;plugin&amp;gt; --marketplace &amp;lt;source&amp;gt;, which offers to add the marketplace before installing the plugin&lt;/p&gt;
120+&lt;p&gt;• Fixed a restored memory file's age note changing between requests after a compaction or resume, which caused prompt cache misses&lt;/p&gt;
121+&lt;p&gt;• Fixed --forward-subagent-text stream-json and SDK output dropping the messages of subagents spawned by a context: fork skill, and of forked skills invoked by a subagent or another forked skill&lt;/p&gt;
122+&lt;p&gt;• Fixed @-mention file suggestions being buried below MCP resources when using a custom fileSuggestion command or typing @./@./&lt;/p&gt;
123+&lt;p&gt;• Fixed fullscreen mode placing background-task completion notices beneath a long turn's collapsed tool row instead of where they arrived; each notice now closes the open row&lt;/p&gt;
124+&lt;p&gt;• Fixed claude plugin marketplace update deleting a GitHub marketplace's local copy when the fetch failed and the marketplace was named after its repository&lt;/p&gt;
125+&lt;p&gt;• Fixed plugin and marketplace messages, logs and claude plugin marketplace list showing a password or token stored in a git, ssh or marketplace URL&lt;/p&gt;
126+&lt;p&gt;• Fixed a resumed cloud session leaving an unanswered question open in the transcript after a queued message superseded it&lt;/p&gt;
127+&lt;p&gt;• Fixed vim mode placing the cursor one character right after a dot-repeated "!" or a fast-typed "i!" switched a non-empty prompt into shell mode&lt;/p&gt;
128+&lt;p&gt;• Fixed fullscreen mode freezing or blanking for several seconds when scrolling up past a large file diff&lt;/p&gt;
129+&lt;p&gt;• Fixed a stray &amp;lt;/ccmemory&amp;gt;-style closing tag occasionally appearing in responses&lt;/p&gt;
130+&lt;p&gt;• Fixed plugin messages, logs and the VS Code plugin dialog showing the wrong server for some git addresses&lt;/p&gt;
131+&lt;p&gt;• Fixed a terminal API Error: 400 on every turn for users behind a network gateway that rewrites API error responses when a beta request header is rejected&lt;/p&gt;
132+&lt;p&gt;• Fixed sandboxed Bash commands on Linux reporting exit code 0 for failed commands when the shell is zsh&lt;/p&gt;
133+&lt;p&gt;• Fixed the Read tool hanging instead of reporting an error when part of a large file could not be decoded under memory pressure&lt;/p&gt;
134+&lt;p&gt;• Fixed --resume, the resume picker preview, resumed background agents and the transcript view failing on a session whose saved history contains a malformed task-reminder or @-file attachment entry&lt;/p&gt;
135+&lt;p&gt;• Fixed a crash when resuming a conversation whose transcript contains a malformed message entry, and a fullscreen crash when such a conversation received new messages while scrolled up&lt;/p&gt;
136+&lt;p&gt;• Fixed sessions failing to resume or start when their saved transcript contains a malformed message content block&lt;/p&gt;
137+&lt;p&gt;• Fixed Grep, Glob and @-file suggestions hanging or running out of memory on searches over the 20MB output cap, and system ripgrep reporting "no matches" instead of an error after a flood of warnings&lt;/p&gt;
138+&lt;p&gt;• Fixed /rewind in a forked or background session restoring a zero-filled or truncated file when the session's file-history backups could not be fully copied&lt;/p&gt;
139+&lt;p&gt;• Fixed fullscreen sessions sometimes exiting with "Claude Code exited after an unrecoverable interface error" when typing fast or holding a key with the slash-command dropdown open&lt;/p&gt;
140+&lt;p&gt;• Fixed background sessions crashing and restarting their worker when a command fed through stdin ran on a machine that had run out of file descriptors&lt;/p&gt;
141+&lt;p&gt;• Fixed a crash at launch when ~/.claude.json holds a malformed mcpNeedsAuthNoticed value&lt;/p&gt;
142+&lt;p&gt;• Fixed --resume and --continue dropping a conversation's earlier thinking when a built-in tool it started with has since been switched off by a server-side flag&lt;/p&gt;
143+&lt;p&gt;• Fixed text selected with the mouse in the fullscreen claude --resume session picker never reaching the clipboard&lt;/p&gt;
144+&lt;p&gt;• Fixed plugin reload previews replacing a running session's extracted plugin files when the plugin was loaded from a --plugin-dir or --plugin-url archive&lt;/p&gt;
145+&lt;p&gt;• Fixed self-hosted runners with --drain-wait-sec losing the final result of a turn that finished during a SIGTERM drain; the runner now waits briefly for the turn to be reported&lt;/p&gt;
146+&lt;p&gt;• Fixed SubagentStop hooks with a specific matcher firing for every stopping subagent whose agent type was empty&lt;/p&gt;
147+&lt;p&gt;• Fixed sandboxed Bash commands being unable to write to project directories named hooks/ or config/&lt;/p&gt;
148+&lt;p&gt;• Fixed Artifact updates failing with "File not found" after a session resumes on another machine or its scratchpad is cleared: the page's last published version is restored&lt;/p&gt;
149+&lt;p&gt;• Fixed /update-config writing Write(path) permission rules, which file permission checks don't match, instead of Edit(path) rules&lt;/p&gt;
150+&lt;p&gt;• Fixed four dead documentation URLs (Pricing, Computer Use, Skills, CLI) in the bundled claude-api skill's live-sources table&lt;/p&gt;
151+&lt;p&gt;• Improved prompt caching for a --system-prompt that contains a __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ line: the text above it is now cached globally, as the SDK's array form already is&lt;/p&gt;
152+&lt;p&gt;• Improved the /desktop error when Claude Desktop does not open: it now says why and what to do next&lt;/p&gt;
153+&lt;p&gt;• Improved the Artifact tool's publish and read results: they now say who can open the page and what the owner's Share menu offers&lt;/p&gt;
154+&lt;p&gt;• Improved artifact publish results: they name the tab icon sent, warn when the page contains a NUL byte, and retry a flaky fetch of the newer page to merge after a stale publish&lt;/p&gt;
155+&lt;p&gt;• Improved pasted and attached images: they are now saved where Claude can open them as files without a permission prompt, including in Desktop and VS Code&lt;/p&gt;
156+&lt;p&gt;• Improved the Artifact tool's guidance so Claude updates a shared artifact in place when you were given edit access to it, instead of publishing a separate copy&lt;/p&gt;
157+&lt;p&gt;• Improved plan-usage reads: editor windows and non-interactive sessions on one machine now share a read made in the last minute instead of each calling the usage endpoint&lt;/p&gt;
158+&lt;p&gt;• Improved the ListPlugins tool description so Claude knows it lists plugins enabled on your claude.ai account, not plugins installed locally with /plugin&lt;/p&gt;
159+&lt;p&gt;• Improved responsiveness when the terminal is slow or paused: output no longer falls further behind while the terminal catches up&lt;/p&gt;
160+&lt;p&gt;• Improved Write and Edit results for files in the synced account-skills folder: they now say the change is not saved to your account and how to save it&lt;/p&gt;
161+&lt;p&gt;• Updated /logout for Claude apps gateway sign-ins to also end the session on gateways that advertise token revocation&lt;/p&gt;
162+&lt;p&gt;• Changed hosted sessions to keep an unanswered permission prompt up after a container restart, instead of asking again&lt;/p&gt;
163+&lt;p&gt;• Changed the Artifact tool to ask for a one-word tab icon on a first publish instead of an emoji favicon&lt;/p&gt;
164+&lt;p&gt;• Changed Claude in Chrome in auto mode to skip the extension's per-site check for classifier-approved calls, as bypass mode does, fixing browser_batch "Permission denied" after a redirect&lt;/p&gt;
165+&lt;p&gt;• Changed plugins installed from an npm source to be fetched with npm pack --ignore-scripts and integrity-verified, so a package's install scripts no longer run&lt;/p&gt;
166+&lt;p&gt;• Changed scheduled and Run now routine runs to save data to, and republish the page of, an artifact you can edit without asking; public artifacts, first publishes and deletes still ask&lt;/p&gt;
167+&lt;p&gt;• Removed the startup notice that told you a one-off scheduled routine had run since your last session&lt;/p&gt;
168+&lt;p&gt;• [VSCode] Added viewing, editing and deleting a saved memory inside the Memory dialog&lt;/p&gt;
169+&lt;p&gt;• [VSCode] Added sending an attached image without typing any text&lt;/p&gt;
170+&lt;p&gt;• [VSCode] Added a Retry link to the MCP servers dialog when the server list fails to load&lt;/p&gt;
171+&lt;p&gt;• [VSCode] Added accept and reject buttons under each change in the proposed-change diff tab, so an edit can be reviewed change by change&lt;/p&gt;
172+&lt;p&gt;• [VSCode] Fixed the transcript creeping toward the bottom in small steps while a permission card waits and content keeps arriving&lt;/p&gt;
173+&lt;p&gt;• [VSCode] Fixed rewound and forked conversations not keeping the permission mode you had picked for the original conversation&lt;/p&gt;
174+&lt;p&gt;• [VSCode] Fixed an empty CLAUDE_CONFIG_DIR entry in the environmentVariables setting making Claude Code keep its files in the workspace&lt;/p&gt;
175+&lt;p&gt;• [VSCode] Fixed plugin install links opening the Manage plugins dialog for plugin names and marketplace addresses that can't be used in a link&lt;/p&gt;
176+&lt;p&gt;• [VSCode] Fixed Remote Control staying shown as connected after a turn-off that Claude Code reported as failed; it now shows as off&lt;/p&gt;
177+&lt;p&gt;• [VSCode] Fixed the scroll to the bottom on send stopping short of the reply when the reply starts arriving during the scroll&lt;/p&gt;
178+&lt;p&gt;• [VSCode] Fixed the agent map showing agents a crash left unfinished as stopped instead of failed once the session is reopened&lt;/p&gt;
179+&lt;p&gt;• [VSCode] Fixed the "Continuing the step" notice not appearing, and the continue limit resetting, after a reload that follows a crash with background tasks still running&lt;/p&gt;
180+&lt;p&gt;• [VSCode] Fixed the session list showing when a session was last reopened, such as after a window reload, instead of when its last message was sent&lt;/p&gt;
181+&lt;p&gt;• [VSCode] Fixed "Fork conversation from here" failing on the message right after one sent while Claude was working&lt;/p&gt;
182+&lt;p&gt;• [VSCode] Fixed the prompt cache clock showing too few minutes after reopening a session with a message sent while Claude was working&lt;/p&gt;
183+&lt;p&gt;• [VSCode] Fixed a background agent that finished while Claude was running a tool losing its completion notice, and its result on the agent map, after a window reload&lt;/p&gt;
184+&lt;p&gt;• [VSCode] Fixed a rare case where text selected in a git-ignored file could be sent to Claude after the extension was unresponsive for several seconds&lt;/p&gt;
185+&lt;p&gt;• [VSCode] Fixed renaming a running session reverting to the generated name (regression in 2.1.269)&lt;/p&gt;
186+&lt;p&gt;• [VSCode] Fixed some claude.ai/code sessions opening in VS Code as an empty conversation with no messages&lt;/p&gt;
187+&lt;p&gt;• [VSCode] Fixed slash commands typed while Claude is responding being sent to the model as text instead of running once the response finishes&lt;/p&gt;
188+&lt;p&gt;• [VSCode] Fixed unreadable code in the plan preview and the Hooks and Permission rules dialogs with the High Contrast Light theme&lt;/p&gt;
189+&lt;p&gt;• [VSCode] Fixed /remote-control being ignored while Remote Control is still connecting: running it again now turns Remote Control off immediately&lt;/p&gt;
190+&lt;p&gt;• [VSCode] Fixed the conversation pulling you back to the bottom while a reply streams after you scroll up, and added a claudeCode.scrollToBottomOnSend setting to turn off the jump on send&lt;/p&gt;
191+&lt;p&gt;• [VSCode] Fixed the Manage plugins dialog showing a password or token that was typed into a marketplace URL&lt;/p&gt;
192+&lt;p&gt;• [VSCode] Improved the agent map: the pill counts running agents and turns red after a failure, the main agent stays in view while the map scrolls, and agents sort by state then end time&lt;/p&gt;
193+&lt;p&gt;• [VSCode] Changed New session in a Claude editor tab to open in the sidebar when Preferred Location is set to Sidebar, instead of always opening another tab&lt;/p&gt;
194+&lt;p&gt;• [VSCode] Changed a message sent while Claude is working to wait at the bottom of the conversation until Claude starts on it&lt;/p&gt;
195+&lt;p&gt;• [Claude Code on the web] Added a "New routine" button to the page shown when a routine link no longer resolves, next to the link back to your routines list&lt;/p&gt;
196+&lt;p&gt;• [Claude Code on the web] Fixed routine "paused" and "on hold" notifications being cut off mid-sentence; the paused-subscription notice now says to turn the routine back on yourself&lt;/p&gt;
197+&lt;p&gt;• [Claude Code on the web] Fixed cloud environments with a very long allowed-domains list saving fine and then failing every session start; saving now fails up front and says how much to trim&lt;/p&gt;
198+&lt;p&gt;• [Claude Code on the web] Fixed Claude's guidance when a cloud session on a personal account is denied GitHub access: it now links to claude.ai/connect-github instead of an admin settings page&lt;/p&gt;
199+&lt;p&gt;• [Claude Code on the web] Improved what Claude tells you when asked to edit, delete or run a routine it didn't create: it now links to the routine's page so you can do it yourself&lt;/p&gt;
200+&lt;p&gt;• [Claude Tag] Added attach conditions for access bundles in Claude Tag settings: an Owner can let a bundle also apply in channels with guests or Slack Connect channels, not just member-only&lt;/p&gt;
201+&lt;p&gt;• [Claude Tag] Added Amazon CloudWatch, CloudWatch Logs, Amazon SNS, Google Cloud Monitoring and Cloud Logging presets to an access bundle's Credentials tab in Claude Tag admin settings&lt;/p&gt;
202+&lt;p&gt;• [Claude Tag] Added Datadog presets for the US3, AP1, AP2 and US1-FED sites; new Datadog connections are now limited to Datadog's read and query API routes&lt;/p&gt;
203+&lt;p&gt;• [Claude Tag] Fixed S3 uploads from recent AWS CLI and SDK versions failing with a 502 error when sent through an AWS connection&lt;/p&gt;
204+&lt;p&gt;• [Claude Tag] Fixed Claude treating a channel as inactive, and skipping untagged messages there, while it was still posting in that channel from a routine or a thread&lt;/p&gt;
205+&lt;p&gt;• [Claude Tag] Fixed a thread's "Claude [task]" display name reverting to plain "Claude" after the session behind that thread was refreshed or restarted&lt;/p&gt;
206+&lt;p&gt;• [Claude Tag] Fixed the model you switched to in a Slack thread silently reverting to the channel's default after that thread's session was restarted or refreshed&lt;/p&gt;
207+&lt;p&gt;• [Claude Tag] Fixed Claude sometimes replying twice when another app or bot @mentioned it in a top-level channel message&lt;/p&gt;
208+&lt;p&gt;• [Claude Tag] Improved Claude's notices in Enterprise Grid channels shared across workspaces: they now say when no workspace is set up yet, or why only organization defaults apply&lt;/p&gt;
209+&lt;p&gt;• [Code Review] Fixed reviews occasionally dropping part of their analysis when one of the reviewing agents returned its findings in an unexpected format&lt;/p&gt;
210+&lt;p&gt;• [Code Review] Fixed pull requests with more than 100 Claude reviews getting a full re-review on every clean merge from the base branch instead of the lighter merge-focused review&lt;/p&gt;</content>
211+ </entry>
212+ <entry>
213+ <id>https://github.com/anthropics/claude-code/releases/tag/v2.1.274</id>
214+ <title>Claude Code v2.1.274</title>
215+ <link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.274"/>
216+ <updated>2026-09-17T00:11:54Z</updated>
217+ <content type="html">&lt;p&gt;• Added a visible warning when memory usage is critical, with steps to free memory or restart safely&lt;/p&gt;
218+&lt;p&gt;• Added CLAUDE_CODE_MCP_STARTUP_WAIT_MS to bound how long the first non-interactive turn waits for connecting MCP servers (0 = don't wait)&lt;/p&gt;
219+&lt;p&gt;• Added effort attribute to the claude_code.llm_request OpenTelemetry trace span, matching the api_request event&lt;/p&gt;
220+&lt;p&gt;• Added claude_code.managed_settings_resolved OTel event: managed-settings sources and policy helper state; redacted settings and digests with OTEL_LOG_MANAGED_SETTINGS=1&lt;/p&gt;
221+&lt;p&gt;• Added store.connect_timeout_seconds to the Claude apps gateway config to lengthen the Postgres connect timeout (default 5 seconds), and improved the boot error when the database is unreachable to point to store.postgres_url and the configured timeout&lt;/p&gt;
222+&lt;p&gt;• Added enduser.sub, the IdP subject, to the telemetry Claude Desktop and Cowork send through a Claude apps gateway&lt;/p&gt;
223+&lt;p&gt;• Added a Claude apps gateway warning when a replica has more requests open than the 256 it sends upstream at once, and a startup log line showing that limit&lt;/p&gt;
224+&lt;p&gt;• Added click-to-expand for collapsed teammate and agent messages in fullscreen mode&lt;/p&gt;
225+&lt;p&gt;• Fixed sessions getting stuck endlessly retrying "unexpected tool_use_id" 400 errors: corrupted transcripts now self-heal where possible, and otherwise a clear error (with a /rewind hint) ends the loop&lt;/p&gt;
226+&lt;p&gt;• Fixed MCP servers configured as http that only speak legacy HTTP+SSE failing to connect when they answer the first request with 422 or another 4xx error&lt;/p&gt;
227+&lt;p&gt;• Fixed Streamable HTTP MCP tool calls timing out after about 5 minutes even when a longer per-server timeout was set&lt;/p&gt;
228+&lt;p&gt;• Fixed MCP prompts and resources not refreshing when a server sends list-changed notifications without declaring listChanged&lt;/p&gt;
229+&lt;p&gt;• Fixed MCP tool calls refused with 403 insufficient_scope being reported as an expired sign-in: the error now names the missing permissions and points to /mcp re-authentication&lt;/p&gt;
230+&lt;p&gt;• Fixed hook-driven sessions (such as an active /goal) ending with "Prompt is too long" instead of compacting when the context overflowed again after a reactive compaction&lt;/p&gt;
231+&lt;p&gt;• Fixed an active /goal being lost when resuming (--continue / --resume) a session that had compacted&lt;/p&gt;
232+&lt;p&gt;• Fixed claude agents losing --model, --effort, --permission-mode, --allow-dangerously-skip-permissions and --agent after an auto-update relaunch&lt;/p&gt;
233+&lt;p&gt;• Fixed a per-turn slowdown when a language server publishes project-wide diagnostics for thousands of files&lt;/p&gt;
234+&lt;p&gt;• Fixed subagents with model: "opus" on Bedrock, Vertex or Foundry leaving the session's model when its id has no recognizable model family (unless ANTHROPIC_DEFAULT_OPUS_MODEL is set)&lt;/p&gt;
235+&lt;p&gt;• Fixed self-hosted runner sessions failing every turn with a 401 after a few failed token refreshes, until the next scheduled refresh; the runner now keeps retrying, and fetches a new token after a 401&lt;/p&gt;
236+&lt;p&gt;• Fixed clickable links to local file paths doing nothing in VS Code and other terminals that require a file:// URI&lt;/p&gt;
237+&lt;p&gt;• Fixed the transcript renumbering ordered lists in your own messages (typing "3. 2. 1." displayed "3. 4. 5."); numbers and "N)" markers now show as typed&lt;/p&gt;
238+&lt;p&gt;• Fixed AskUserQuestion preview notes being attached to a previously chosen option instead of the highlighted one&lt;/p&gt;
239+&lt;p&gt;• Fixed AskUserQuestion preview mode dropping the highlighted option when submitting a note with Enter&lt;/p&gt;
240+&lt;p&gt;• Fixed a resumed background agent keeping half of an interrupted tool batch when one of its calls was approved with a message&lt;/p&gt;
241+&lt;p&gt;• Fixed a local claude -p --resume started with CLAUDE_CODE_RESUME_INTERRUPTED_TURN not reporting background tasks the previous process left unfinished&lt;/p&gt;
242+&lt;p&gt;• Fixed the first turn of a cloud session sometimes starting without the tools of an SDK-hosted MCP server that was still connecting&lt;/p&gt;
243+&lt;p&gt;• Fixed background agent notifications claiming the agent had no live background work when it was still waiting on its own background task and would resume&lt;/p&gt;
244+&lt;p&gt;• Fixed error hints in Claude Desktop sessions to suggest slash commands like /usage-credits instead of CLI flags that cannot be used there&lt;/p&gt;
245+&lt;p&gt;• Fixed /schedule saving a routine's prompt without its message role when Claude writes the routine in the shape that listing routines returns&lt;/p&gt;
246+&lt;p&gt;• Fixed /status not showing the apiKeyHelper failure that its own error banner told you to check&lt;/p&gt;
247+&lt;p&gt;• Fixed /fast on in non-interactive sessions reporting on and then turning off under an organization's managed fast mode policy; it now says the organization has disabled it&lt;/p&gt;
248+&lt;p&gt;• Fixed the Artifact tool asking you to approve an update to an artifact that it then refused because the session had not read the latest version&lt;/p&gt;
249+&lt;p&gt;• Fixed Cowork and claude.ai cloud sessions with network access on treating reads of a teammate's artifact as if network access were off&lt;/p&gt;
250+&lt;p&gt;• Fixed a plugin or marketplace directory with no git repository of its own taking its version from an enclosing git repository, such as a git-managed ~/.claude&lt;/p&gt;
251+&lt;p&gt;• Fixed --strict-mcp-config with an empty --mcp-config holding the first non-interactive turn for up to MCP_TIMEOUT on incidental MCP servers&lt;/p&gt;
252+&lt;p&gt;• Fixed Stop prompt hooks re-sending their whole prompt on every block in a conversation; repeat blocks now name the condition with a 500-character label&lt;/p&gt;
253+&lt;p&gt;• Fixed extra empty editor windows opening at startup on Linux under Wayland when running inside the Cursor or VS Code terminal&lt;/p&gt;
254+&lt;p&gt;• Fixed an unhandled promise rejection in the Claude apps gateway when Postgres drops a connection during a spend check&lt;/p&gt;
255+&lt;p&gt;• Fixed Claude apps gateway cutting every open stream on SIGTERM: it now lets in-flight requests finish for up to 25 seconds before exiting (CLAUDE_GATEWAY_DRAIN_TIMEOUT_MS)&lt;/p&gt;
256+&lt;p&gt;• Fixed installed_plugins.json being rewritten on nearly every start-up when plugin policy comes from remote managed settings, which made Claude Desktop reload every open session's plugins&lt;/p&gt;
257+&lt;p&gt;• Fixed headless and SDK sessions making a separate model call for every background task that finished; completions already queued are now answered by one call&lt;/p&gt;
258+&lt;p&gt;• Fixed the Bash tool re-sourcing the shell profile (a multi-second stall on the next command) after every plugin reload; it now does so only when the plugins' bin/ directories changed&lt;/p&gt;
259+&lt;p&gt;• Fixed plugins with a top-level $schema in hooks/hooks.json showing an "unknown key" notice&lt;/p&gt;
260+&lt;p&gt;• Fixed MCP connection errors and the MCP login tool's description showing secrets resolved from ${VAR} placeholders in MCP configs&lt;/p&gt;
261+&lt;p&gt;• Fixed Bash permission checks for commands that loop over or assign certain special shell variables; these commands now ask for permission&lt;/p&gt;
262+&lt;p&gt;• Fixed worktree-isolated sessions accepting Bash commands with certain nested shell expansions; these are now refused&lt;/p&gt;
263+&lt;p&gt;• Fixed the Edit permission prompt preview sometimes showing a different location than the approved edit in files with multi-byte characters&lt;/p&gt;
264+&lt;p&gt;• Fixed background commands being stopped after 30 idle minutes on machines under mild memory pressure; they're now stopped only when memory is critically low, and the debug log says why&lt;/p&gt;
265+&lt;p&gt;• Fixed a message a subagent sends to the main session disappearing from the Claude Desktop transcript after a relaunch&lt;/p&gt;
266+&lt;p&gt;• Fixed a plugin loaded from a .zip being served from a stale extraction after several overlapping reloads&lt;/p&gt;
267+&lt;p&gt;• Fixed a sub-agent's progress summary being replaced by a runaway multi-paragraph reply&lt;/p&gt;
268+&lt;p&gt;• Improved startup in --input-format stream-json sessions: the first turn no longer waits up to 2s for still-connecting MCP servers whose tools tool search defers; they arrive on a later turn&lt;/p&gt;
269+&lt;p&gt;• Improved Monitor tool notifications: a script's final output and its exit now arrive as one notification instead of two, saving a model turn&lt;/p&gt;
270+&lt;p&gt;• Improved Artifact tool errors: when you are not signed in to claude.ai the terminal now says so on the first attempt, and Claude is told to stop retrying a rejected call sooner&lt;/p&gt;
271+&lt;p&gt;• Improved artifact publishing: a publish built on an older version is stopped before it is sent, with the newer page to merge&lt;/p&gt;
272+&lt;p&gt;• Improved safety checks before removing an agent worktree that contains submodule checkouts&lt;/p&gt;
273+&lt;p&gt;• Improved OTEL_LOG_RAW_API_BODIES=file:&amp;lt;dir&amp;gt; output: a new index.jsonl and request_body_id / message.id event attributes link each response to its request file and transcript message&lt;/p&gt;
274+&lt;p&gt;• Improved Claude apps gateway boot: it now tries the first Postgres connection up to three times before exiting, so a database that is reachable a few seconds late no longer fails the boot&lt;/p&gt;
275+&lt;p&gt;• Improved the Claude apps gateway's spend-limit check under load: it now takes one database round trip instead of four, so fewer checks time out on a busy gateway&lt;/p&gt;
276+&lt;p&gt;• Improved Claude apps gateway sign-in rate limit errors: /login now explains the refusal, and the gateway log says which limit was hit and which setting to change&lt;/p&gt;
277+&lt;p&gt;• Changed Bedrock, Vertex, Foundry and telemetry-disabled installs to use the v2 MCP client and MCP 2026-07-28 negotiation with direct HTTP servers by default, as other installs already do (opt out: MCP_SDK_GENERATION=v1 or MCP_PROTOCOL_NEGOTIATION=legacy)&lt;/p&gt;
278+&lt;p&gt;• Changed /code-review to use leaner inline review prompts for every model that has no tuned settings of its own, instead of spawning many review subagents&lt;/p&gt;
279+&lt;p&gt;• Changed "type": "sdk" MCP entries in .mcp.json, settings, plugins and agent files to be skipped with a warning: only an SDK host application can register in-process servers&lt;/p&gt;
280+&lt;p&gt;• Changed artifact watching in local sessions: a new version published elsewhere no longer starts a turn; Claude learns of it from a later Artifact tool result&lt;/p&gt;
281+&lt;p&gt;• Changed plugin and marketplace clones to leave Git LFS files as pointers instead of downloading them; git lfs pull in the checkout fetches them&lt;/p&gt;
282+&lt;p&gt;• Changed self-hosted runners to skip a read-only repository the git host refuses at the access check instead of failing the session start&lt;/p&gt;
283+&lt;p&gt;• Changed the /status GitHub line to read "Cloud sessions", and /web-setup, /ultrareview, and teleport messages to say "cloud session" instead of "Claude Code on the web"&lt;/p&gt;
284+&lt;p&gt;• [VSCode] Added continuation of the step a window reload interrupted, labeled in the chat, with a Claude Code: Continue After Reload setting to turn it off&lt;/p&gt;
285+&lt;p&gt;• [VSCode] Added Memory and Instructions entries to the Customize menu: Memory shows the auto-memory toggles, the saved memories and the memory folders, and Instructions edits the CLAUDE.md files&lt;/p&gt;
286+&lt;p&gt;• [VSCode] Added a claudeCode.lockEditorGroups setting to stop Claude from locking the editor groups it opens in&lt;/p&gt;
287+&lt;p&gt;• [VSCode] Fixed a /btw side question asked in a new conversation's first seconds occasionally showing another session's side-question history&lt;/p&gt;
288+&lt;p&gt;• [VSCode] Fixed a brief freeze when the extension first looks up your global gitignore file&lt;/p&gt;
289+&lt;p&gt;• [VSCode] Fixed a message sent while Claude was running a tool disappearing from the conversation after a window reload&lt;/p&gt;
290+&lt;p&gt;• [VSCode] Fixed the Manage Plugins enable toggle and MCP servers dialog rows being unreachable from the keyboard&lt;/p&gt;
291+&lt;p&gt;• [VSCode] Fixed sign-ins and sign-outs made in a terminal not showing until a reload after CLAUDE_CONFIG_DIR changed in the Environment Variables setting&lt;/p&gt;
292+&lt;p&gt;• [VSCode] Fixed Edit diffs in the chat being cut off at the bottom at some panel widths and for long wrapped lines; diff boxes now fit the rows shown&lt;/p&gt;
293+&lt;p&gt;• [VSCode] Fixed overlapping settings writes from the extension leaving ~/.claude/settings.json unparseable or dropping a setting&lt;/p&gt;
294+&lt;p&gt;• [VSCode] Fixed Open in New Tab (Ctrl/Cmd+Shift+Esc) sometimes leaving the new tab's message box unfocused, so typing went nowhere until you clicked it&lt;/p&gt;
295+&lt;p&gt;• [VSCode] Fixed reopening a closed Claude tab splitting the editor layout when its locked group still holds another Claude tab and a file&lt;/p&gt;
296+&lt;p&gt;• [VSCode] Fixed New session opening another locked editor group whenever a file tab shared the group with your Claude tab&lt;/p&gt;
297+&lt;p&gt;• [VSCode] Fixed session names shifting sideways in the session picker while typing a search query&lt;/p&gt;
298+&lt;p&gt;• [VSCode] Fixed the plan review card cutting off its Send feedback button and reason field when a plan has several comments; the comment list now scrolls&lt;/p&gt;
299+&lt;p&gt;• [VSCode] Fixed inline code and code blocks in chat replies being unreadable under the High Contrast themes&lt;/p&gt;
300+&lt;p&gt;• [VSCode] Improved screen reader navigation of the conversation: each message is announced as "You" or "Claude", with the tool name for tool steps&lt;/p&gt;
301+&lt;p&gt;• [VSCode] Changed the default global gitignore file to $XDG_CONFIG_HOME/git/ignore when XDG_CONFIG_HOME is an absolute path&lt;/p&gt;
302+&lt;p&gt;• [Claude Code on the web] Added a "Compare against" branch picker to a cloud session's diff view, so you can diff its changes against any branch instead of only the base branch&lt;/p&gt;
303+&lt;p&gt;• [Claude Code on the web] Fixed git operations in cloud sessions failing with "service unavailable" when GitHub's token renewal briefly errors&lt;/p&gt;
304+&lt;p&gt;• [Claude Code on the web] Fixed editing a routine occasionally making it fire twice or re-enabling a routine that had just been paused&lt;/p&gt;
305+&lt;p&gt;• [Claude Code on the web] Fixed commits in cloud sessions occasionally failing with a signing error for a few minutes after the session's credentials refreshed&lt;/p&gt;
306+&lt;p&gt;• [Claude Code on the web] Fixed the toast after saving a routine whose GitHub trigger couldn't be linked to show the reason, such as a per-repository trigger limit, instead of only "edit to retry"&lt;/p&gt;
307+&lt;p&gt;• [Claude Code on the web] Fixed sessions sometimes flipping back to unread right after you mark them read&lt;/p&gt;
308+&lt;p&gt;• [Claude Code on the web] Changed routines to skip a run and retry for up to 72 hours when the owner's GitHub connection is missing, instead of switching the routine off at the first failed check&lt;/p&gt;
309+&lt;p&gt;• [Claude Code on the web] Changed a routine's on-hold notice: when your subscription is paused it now tells you to turn the routine back on yourself instead of promising an automatic resume&lt;/p&gt;
310+&lt;p&gt;• [Claude Tag] Added a Guests setting to the Add channel and Add workspace forms in Claude Tag admin settings, so owners can pick Inherit, Allow, Channel only or Restrict up front&lt;/p&gt;
311+&lt;p&gt;• [Claude Tag] Fixed Claude not answering when another Slack app or bot @mentions it; the tag now gets a reply and wakes Claude in a channel it had stopped following after days of inactivity&lt;/p&gt;
312+&lt;p&gt;• [Claude Tag] Fixed Claude missing another app's message that tagged @Claude right after a new Slack channel was created; it's now delivered once Claude has joined&lt;/p&gt;
313+&lt;p&gt;• [Claude Tag] Fixed Claude folding a follow-up sent minutes after its last Slack message into it as a silent edit; late updates such as blockers now post as a new reply that notifies&lt;/p&gt;
314+&lt;p&gt;• [Claude Tag] Fixed Claude's Slack search failing with an error whenever it searched within a single channel; it now returns that channel's matching messages&lt;/p&gt;
315+&lt;p&gt;• [Claude Tag] Fixed a safety-filter stop silently resetting a Slack thread's context when nobody was waiting; Claude now always says so and no longer cancels background work still running&lt;/p&gt;
316+&lt;p&gt;• [Claude Tag] Fixed email addresses in Claude's Slack replies rendering with a visible mailto: prefix; they now show as the plain, clickable address&lt;/p&gt;
317+&lt;p&gt;• [Claude Tag] Fixed Claude refusing to watch an Enterprise Grid channel shared with the whole organization when asked from another workspace in the grid&lt;/p&gt;
318+&lt;p&gt;• [Claude Tag] Fixed the Environment picker in Claude Tag admin settings showing a raw environment ID instead of the environment's name for archived or app-created environments&lt;/p&gt;
319+&lt;p&gt;• [Claude Tag] Improved Claude's live progress checklist in Slack: capped at 2,000 characters, reposted at most every 15 minutes in busy threads, with older "Latest task list" links updated&lt;/p&gt;
320+&lt;p&gt;• [Claude Tag] Removed the repeated guest-attribution note Claude appended to a Slack canvas each time it edited one in a channel using the "Channel only" guest setting&lt;/p&gt;
321+&lt;p&gt;• [Code Review] Fixed re-reviews occasionally leaving a fixed finding's thread open when the new review also filed a lower-severity note under it&lt;/p&gt;
322+&lt;p&gt;• [Code Review] Fixed rare reviews ending with "Code review encountered an error" when GitHub or an internal service failed transiently at launch; they now wait and retry&lt;/p&gt;
323+&lt;p&gt;• [Code Review] Improved how Code Review words each posted finding: short plain sentences that say who is affected, where the code goes wrong, and the fix up front&lt;/p&gt;
324+&lt;p&gt;• [Code Review] Improved the check-run card and PR comment when a review is skipped because of an organization limit: each cause now links the admin page that fixes it&lt;/p&gt;</content>
325+ </entry>
326+ <entry>
11327 <id>https://github.com/anthropics/claude-code/releases/tag/v2.1.273</id>
12328 <title>Claude Code v2.1.273</title>
13329 <link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.273"/>
@@ -844,182 +1160,4 @@
8441160 &lt;p&gt;• Fixed Remote Control sessions hosted by Claude Desktop or VS Code stalling for minutes after a tool finished when the connection to claude.ai was degraded&lt;/p&gt;
8451161 &lt;p&gt;• Fixed background task notifications with very large failure output (for example git errors on a full disk) making the conversation exceed the API request size limit&lt;/p&gt;</content>
8461162 </entry>
847- <entry>
848- <id>https://github.com/anthropics/claude-code/releases/tag/v2.1.251</id>
849- <title>Claude Code v2.1.251</title>
850- <link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.251"/>
851- <updated>2026-08-28T18:19:26Z</updated>
852- <content type="html">&lt;p&gt;• Added PreModelSwitch and PostModelSwitch hook events (block, confirm, or annotate a model switch); SessionStart resume hooks now receive session staleness and the estimated re-cache cost&lt;/p&gt;
853-&lt;p&gt;• Added live streaming of a foreground subagent's tool calls and results to Remote Control clients (background subagents, the default, still show status only)&lt;/p&gt;
854-&lt;p&gt;• Added a Spend limit bar to /usage and a rate_limits.spend_limit status line field for developers behind a Claude apps gateway with spend limits&lt;/p&gt;
855-&lt;p&gt;• Added a per-session prompt-cache line to /cost (hit ratio, misses, tokens re-cached, warm/cold) and a matching prompt_cache object for status line scripts&lt;/p&gt;
856-&lt;p&gt;• Added attach, logs, stop, respawn, and rm to claude --help; the --resume message for a running background session now names the exact claude attach &amp;lt;id&amp;gt; command&lt;/p&gt;
857-&lt;p&gt;• Fixed file tools (Read, Write, Edit) following a symlink swapped inside the working directory after the permission check, which could read or write outside the approved location&lt;/p&gt;
858-&lt;p&gt;• Fixed plugin commands declared in a marketplace entry being able to point outside the plugin directory; such paths are now rejected with a path-traversal error&lt;/p&gt;
859-&lt;p&gt;• Fixed project settings being able to enable detailed beta tracing or raw API body logging, and a lower-scope beta tracing endpoint bypassing an OTLP collector pinned by managed settings or a host app&lt;/p&gt;
860-&lt;p&gt;• Fixed the Workflow tool reading (and quoting in errors) a scriptPath outside what the session may read before the permission check ran&lt;/p&gt;
861-&lt;p&gt;• Fixed Grep and Glob not applying Read(...) deny rules to files reached through a symlinked search path&lt;/p&gt;
862-&lt;p&gt;• Fixed conversations getting stuck on "text content blocks must be non-empty" errors after a turn where the model produced only thinking&lt;/p&gt;
863-&lt;p&gt;• Fixed the first launch on a fresh install starting in default mode instead of auto mode for accounts whose startup default is auto mode&lt;/p&gt;
864-&lt;p&gt;• Fixed Opus 5 requests failing with "effort … is not supported when thinking is disabled" when effort was xhigh/max and thinking was turned off; effort is now sent as high in that case&lt;/p&gt;
865-&lt;p&gt;• Fixed replying to a message Claude Desktop delivered from another session: SendMessage to that session id now delivers through Claude Desktop instead of failing with "not reachable"&lt;/p&gt;
866-&lt;p&gt;• Fixed TUI lag with many parallel subagents: per-second progress ticks now replace their predecessor instead of piling up in the transcript&lt;/p&gt;
867-&lt;p&gt;• Fixed agent teams: a teammate's final answer not reaching the team lead — it now arrives in the idle notification instead of a content-free "available" notice&lt;/p&gt;
868-&lt;p&gt;• Fixed background subagents being unable to reply to a message from an unnamed sibling or parent agent (from was the agent type, which is not an address)&lt;/p&gt;
869-&lt;p&gt;• Fixed managed-settings disableAutoMode arriving mid-session not moving an already-running auto-mode session back to default mode&lt;/p&gt;
870-&lt;p&gt;• Fixed a "switch to Opus 1M for 5x more context" tip that appeared even when the current Opus model already has a 1M context window&lt;/p&gt;
871-&lt;p&gt;• Fixed Claude apps gateway sessions treating a stored Anthropic profile (e.g. a Console sign-in) as active: listing it in /status and retrying gateway 401s with it, though requests never use it&lt;/p&gt;
872-&lt;p&gt;• Fixed cloud sessions telling Claude the model had changed when the host was only setting the session's initial model&lt;/p&gt;
873-&lt;p&gt;• Fixed Remote Control reporting a failure when an organization's policy disables it; it now shows a single quiet notice instead&lt;/p&gt;
874-&lt;p&gt;• Fixed /mcp reconnect on Remote Control showing a generic withheld-detail error instead of the real remedy when a server was disabled in another session&lt;/p&gt;
875-&lt;p&gt;• Fixed --input-format stream-json: client-injected assistant tool calls sent without a message id were merged into the first one and their results lost, including when resuming older sessions&lt;/p&gt;
876-&lt;p&gt;• Fixed session transcripts being silently overwritten when a directory change relocated a session onto an existing same-ID transcript&lt;/p&gt;
877-&lt;p&gt;• Fixed background sessions and their subagents being unable to edit files inside a git worktree they created with git worktree add&lt;/p&gt;
878-&lt;p&gt;• Fixed background sessions occasionally starting without any plugin skills (and staying that way) when another Claude Code process was refreshing the plugin marketplace at the same moment&lt;/p&gt;
879-&lt;p&gt;• Fixed selecting text in an opened background session inside tmux over SSH: it now copies to the tmux buffer like a foreground session instead of falling back to OSC 52&lt;/p&gt;
880-&lt;p&gt;• Fixed SDK and cloud sessions hanging indefinitely when an SDK MCP server's handshake acknowledgment was lost; the wait now times out after 70 seconds and marks only that server failed&lt;/p&gt;
881-&lt;p&gt;• Fixed self-hosted runner leaving a stuck session's Bash tool processes running after the session was force-stopped&lt;/p&gt;
882-&lt;p&gt;• Fixed /usage-credits for Team and Enterprise members whose admin set the org's usage-credit limit to $0: it now offers to ask the admin instead of saying a cap was reached&lt;/p&gt;
883-&lt;p&gt;• Fixed --worktree --tmux with a merge-request number on a gitlab.com origin trying a doomed GitHub-style fetch first instead of fetching the GitLab ref directly&lt;/p&gt;
884-&lt;p&gt;• Fixed Ctrl+G failing with "Emacs quit unexpectedly" in background sessions for editors that open /dev/tty, such as emacs -nw and micro&lt;/p&gt;
885-&lt;p&gt;• Fixed an additionalDirectories entry containing a null byte crashing startup, or breaking /add-dir and later settings updates when it came from an SDK host, IDE, or hook; it is now skipped&lt;/p&gt;
886-&lt;p&gt;• Fixed the MCP server menu's copy shortcut: it now says how the sign-in URL was copied instead of always claiming success&lt;/p&gt;
887-&lt;p&gt;• Fixed italic text (such as the session recap line) rendering as highlighted blocks in GNU screen and in tmux sessions using a screen terminal type&lt;/p&gt;
888-&lt;p&gt;• Fixed claude mcp add --header and claude mcp add-json help text naming the wrong transports&lt;/p&gt;
889-&lt;p&gt;• Fixed claude ultrareview and /ultrareview waiting the full 30 minutes when the cloud session fails to start; they now stop early and report the reason&lt;/p&gt;
890-&lt;p&gt;• Fixed Bash permission checks auto-approving commands that assign an arithmetic expression to an integer shell variable (e.g. OPTIND=1/0, RANDOM=2+2); these now prompt for approval&lt;/p&gt;
891-&lt;p&gt;• Fixed backgrounded sessions (←, /background, --bg) losing a Vertex/Bedrock gateway (ANTHROPIC_*_BASE_URL + CLAUDE_CODE_SKIP_*_AUTH) exported in the shell, so every request failed&lt;/p&gt;
892-&lt;p&gt;• Fixed claude --bg --model fable on Max plans stopping to ask for usage credits while the interactive session on the same account still had Fable allowance&lt;/p&gt;
893-&lt;p&gt;• Fixed the one-time "make auto mode your default" offer appearing in unattended sessions (e.g. agent-team teammate panes), where a stray keypress could accept it unread&lt;/p&gt;
894-&lt;p&gt;• Fixed the managed-settings approval prompt re-appearing after signing in again to the same Claude apps gateway when the settings are unchanged&lt;/p&gt;
895-&lt;p&gt;• Fixed disabled /bug and /share reporting that /feedback was disabled; tips, /help, and refusal messages no longer suggest /feedback when an org policy or env var turns it off&lt;/p&gt;
896-&lt;p&gt;• Fixed cloud session creation advising GitHub setup after a transient GitHub connection failure — the message now says to retry instead&lt;/p&gt;
897-&lt;p&gt;• Improved CPU usage during turns in interactive sessions by cutting redundant UI re-renders&lt;/p&gt;
898-&lt;p&gt;• Improved install size: the native binary is about 5 MB smaller&lt;/p&gt;
899-&lt;p&gt;• Improved cloud sessions: when the session's network proxy drops a connection during a Bash command, the tool result now names the host and reason instead of only "connection reset"&lt;/p&gt;
900-&lt;p&gt;• Improved /schedule to explain that MCP servers configured in Claude Code can't be attached to cloud routines, instead of a bare "No MCP connectors" message&lt;/p&gt;
901-&lt;p&gt;• Improved framing of messages from your own subagents: Claude is told the sender is a worker inside this session, not an unrelated Claude session&lt;/p&gt;
902-&lt;p&gt;• Improved the prompt placeholder to read "Message @name…" while viewing a background subagent or fork transcript opened from the subagent panel or /tasks&lt;/p&gt;
903-&lt;p&gt;• Improved sanitization of MCP server names in error messages, menus, and command results&lt;/p&gt;
904-&lt;p&gt;• Improved Amazon Bedrock session start under CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST (e.g. Claude Desktop): a session given a Bedrock model ID or ARN no longer waits for inference-profile discovery&lt;/p&gt;
905-&lt;p&gt;• Improved the managed settings approval dialog to list only the settings that changed since you last approved them&lt;/p&gt;
906-&lt;p&gt;• Improved retry when the model's tool call is malformed: the broken output is now dropped from the retry context, including on Bedrock, Vertex, and Foundry&lt;/p&gt;
907-&lt;p&gt;• Changed /radio to be available on Bedrock, Vertex AI, Foundry, and Claude Platform on AWS, and when telemetry is disabled&lt;/p&gt;
908-&lt;p&gt;• Changed Claude in Chrome so browser actions always go through Claude Code's permission checks, including in sessions with telemetry disabled, which previously used the Chrome extension's own prompts&lt;/p&gt;
909-&lt;p&gt;• Changed CLAUDE_CODE_SUBAGENT_MODEL to set the default subagent model rather than override everything: an agent definition's model: and an explicit per-spawn model now take precedence over it&lt;/p&gt;
910-&lt;p&gt;• Changed the default commit trailer to Co-Authored-By: Claude Code when the active model isn't a recognized Claude model (e.g. third-party models behind a custom ANTHROPIC_BASE_URL)&lt;/p&gt;
911-&lt;p&gt;• Changed the default model for seat-based Enterprise subscriptions to Opus 5, matching other premium plans&lt;/p&gt;
912-&lt;p&gt;• Changed /effort to save your default effort level per model, so each model keeps its own setting when you switch&lt;/p&gt;
913-&lt;p&gt;• Changed analytics to no longer turn off before sign-in solely because managed settings force gateway login (or cannot be read); they stay off once signed in to the gateway or via DISABLE_TELEMETRY&lt;/p&gt;
914-&lt;p&gt;• Changed the footer PR badge on Bedrock, Vertex, and Foundry, and when telemetry is off, to call the GitHub API directly (via gh auth token, GH_TOKEN, or GITHUB_TOKEN) instead of gh pr view&lt;/p&gt;
915-&lt;p&gt;• Changed how Bash command output files are created and read back when commands run in the sandbox, so a sandboxed command cannot redirect or replace them&lt;/p&gt;
916-&lt;p&gt;• Changed plugin/LSP install suggestions and the auto-mode default offer to wait until you've sent or cleared what you're typing, so the Enter that sends your prompt can't answer them&lt;/p&gt;
917-&lt;p&gt;• Changed server-managed settings that terminate sandbox TLS, route sandbox traffic through your own proxy, inject credentials, or weaken sandbox isolation to require approval before they apply&lt;/p&gt;
918-&lt;p&gt;• Changed ANTHROPIC_CUSTOM_HEADERS from managed or project settings to require approval when it sets a credential, org/tenant, routing, or API-behavior header (e.g. Authorization, Host)&lt;/p&gt;
919-&lt;p&gt;• Changed project-level .claude/settings.json env to no longer set CLAUDE_CONFIG_DIR, CLAUDE_CODE_TMPDIR, or TMPDIR/TMP/TEMP; set them in your shell, user, or managed settings instead&lt;/p&gt;
920-&lt;p&gt;• Removed syntax highlighting for six rarely used languages (1c, gml, isbl, mathematica, maxima, sqf); the binary is 2.5 MB smaller&lt;/p&gt;
921-&lt;p&gt;• [VSCode] Fixed the sign-in screen's "Bedrock, Foundry, or Vertex" button opening the docs at the top of the page instead of the third-party provider setup section&lt;/p&gt;
922-&lt;p&gt;• [VSCode] Changed the Remote Control banner to a footer pill (shown while Remote Control is on or has failed) that opens the session on claude.ai/code; turn it on or off with /remote-control&lt;/p&gt;</content>
923- </entry>
924- <entry>
925- <id>https://github.com/anthropics/claude-code/releases/tag/v2.1.250</id>
926- <title>Claude Code v2.1.250</title>
927- <link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.250"/>
928- <updated>2026-08-28T00:49:09Z</updated>
929- <content type="html">&lt;p&gt;• Bug fixes and reliability improvements&lt;/p&gt;</content>
930- </entry>
931- <entry>
932- <id>https://github.com/anthropics/claude-code/releases/tag/v2.1.248</id>
933- <title>Claude Code v2.1.248</title>
934- <link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.248"/>
935- <updated>2026-08-27T22:12:09Z</updated>
936- <content type="html">&lt;p&gt;• Added --restricted (or CLAUDE_CODE_RESTRICTED=1): removes the built-in tools that run commands or code and WebFetch (unless named in --tools), keeps file tools inside the working directory, refuses bypassPermissions, and ignores user, project and local settings files&lt;/p&gt;
937-&lt;p&gt;• Added experimental.cacheTtl ("5m" or "1h") to agent frontmatter: a per-agent prompt cache TTL used when no subagent TTL setting is configured&lt;/p&gt;
938-&lt;p&gt;• Added claude self-hosted-runner --client-label &amp;lt;label&amp;gt; (or SELF_HOSTED_RUNNER_CLIENT_LABEL) to override the label the runner registers with (default: hostname)&lt;/p&gt;
939-&lt;p&gt;• Added server-managed settings diagnostics: a startup warning when the settings fail to load, and a /doctor and /status line explaining a load failure or why they weren't fetched (Bedrock/Vertex/third-party provider, custom ANTHROPIC_BASE_URL)&lt;/p&gt;
940-&lt;p&gt;• Added a warning in /web-setup when the GitHub CLI token lacks the workflow scope, since pushes to very large repositories can be rejected without it&lt;/p&gt;
941-&lt;p&gt;• Added /usage-credits for Enterprise organizations billed through AWS Marketplace, self-serve Enterprise, and Enterprise trials, so members can request a higher usage limit from their admin&lt;/p&gt;
942-&lt;p&gt;• Added cross-session messaging (SendMessage / ListAgents) between sessions on the same machine on Bedrock, Vertex, and Foundry, and when telemetry is disabled&lt;/p&gt;
943-&lt;p&gt;• Fixed a prompt-cache miss (and lost extended-thinking context) roughly once an hour in long sessions, caused by tool definitions being re-rendered after an OAuth token refresh&lt;/p&gt;
944-&lt;p&gt;• Fixed the ScheduleWakeup tool definition changing between a session and its --resume when the account had entered usage overage, causing a full prompt-cache miss on the resumed session's first turn&lt;/p&gt;
945-&lt;p&gt;• Fixed Claude Desktop and Cowork sessions disappearing after 30 days: the transcript cleanup now keeps desktop-written sessions while they are in the app (unless org policy manages retention); the new desktopSessionCleanupPeriodDays setting caps the exemption&lt;/p&gt;
946-&lt;p&gt;• Fixed being sent to the login screen when another Claude Code process held the token refresh lock while the session token had expired; the request now fails with a retryable error instead&lt;/p&gt;
947-&lt;p&gt;• Windows: Fixed the claude agents list not responding to the keyboard after detaching from a session, or when launched in a terminal tab left in win32-input-mode&lt;/p&gt;
948-&lt;p&gt;• Fixed the recommended Console sign-in in /login failing with an OAuth error before showing a sign-in URL on machines where it can't be used (for example when ANTHROPIC_API_KEY or an API key helper is set); it now falls back to the API-key sign-in&lt;/p&gt;
949-&lt;p&gt;• Fixed model names in /model and fast-mode switch notices to render as code, so suffixes like [1m] display literally instead of as a link&lt;/p&gt;
950-&lt;p&gt;• Fixed claude agents skipping the workspace trust prompt when the CI environment variable is set&lt;/p&gt;
951-&lt;p&gt;• Fixed claude agents crashing on launch when the PR-status cache held a malformed entry&lt;/p&gt;
952-&lt;p&gt;• Fixed agent view resurrecting a weeks-old background session after the machine was off: such a session now shows as stopped at its real end, and opening it asks before resuming its saved conversation&lt;/p&gt;
953-&lt;p&gt;• Fixed agent view sometimes opening an older conversation, and dropping the typed prompt, when starting a new session&lt;/p&gt;
954-&lt;p&gt;• Fixed claude agents: opening a stopped session that you already resumed in another terminal no longer starts a second process on that conversation; the row now says it is open in a terminal&lt;/p&gt;
955-&lt;p&gt;• Fixed claude agents and claude rm refusing to delete a session ("has commits that are not pushed anywhere") when its worktree branch was already merged into your checked-out default branch (e.g. local main) but not yet pushed&lt;/p&gt;
956-&lt;p&gt;• Fixed background sessions waiting silently when a PermissionRequest or PreToolUse hook prints an invalid answer: the claude agents row now names the hook and the schema error&lt;/p&gt;
957-&lt;p&gt;• Fixed hooks silently treating a stdout {…} object that isn't valid JSON as plain text; it's now reported as a hook error with the parse message&lt;/p&gt;
958-&lt;p&gt;• Fixed /mcp listing a project .mcp.json entry that declares the claude.ai connector type under the trusted "claude.ai" heading; it now appears under its real scope&lt;/p&gt;
959-&lt;p&gt;• Fixed MCP servers whose headersHelper supplies the Authorization header falling into OAuth discovery on a 401 instead of re-running the helper and retrying the call as documented&lt;/p&gt;
960-&lt;p&gt;• Fixed /login to a Claude apps gateway hanging when the managed-settings security approval dialog was required&lt;/p&gt;
961-&lt;p&gt;• Fixed gateway model discovery (CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY) never running when apiKeyHelper is the only credential&lt;/p&gt;
962-&lt;p&gt;• Fixed claude logs leaving mouse tracking, bracketed paste and the alternate screen switched on in the terminal it was run from&lt;/p&gt;
963-&lt;p&gt;• Fixed the trust dialog's list of repo permission rules showing a garbled character when a long rule was cut off in the middle of an emoji&lt;/p&gt;
964-&lt;p&gt;• Fixed the permission mode indicator staying hidden behind the "Press Ctrl-C again to exit" hint when you press shift+tab right after ctrl+c&lt;/p&gt;
965-&lt;p&gt;• Fixed /ultrareview and locally seeded cloud sessions uploading uncommitted edits to prod.env-style and *.tfvars files, or to editor swap, temp, and backup copies of credential files (e.g. key.pem.tmp, id_rsa.swo); they now stay on your machine&lt;/p&gt;
966-&lt;p&gt;• Fixed Remote Control sessions occasionally never showing a permission prompt or the latest messages on the connected device after the CLI silently reconnected&lt;/p&gt;
967-&lt;p&gt;• Fixed cloud sessions occasionally failing at startup when the container's session credentials were not yet readable&lt;/p&gt;
968-&lt;p&gt;• Fixed claude remote-control rejecting its own flags (e.g. --spawn, --name) when a global flag or a wrapper-injected option precedes the subcommand&lt;/p&gt;
969-&lt;p&gt;• Fixed startup warnings (e.g. "N MCP servers need authentication") rendering one column right of the rest of the transcript&lt;/p&gt;
970-&lt;p&gt;• Fixed a backgrounded worktree session losing its checkout: the background session now holds the worktree's lock while it runs, so cleanup and git worktree remove leave it alone&lt;/p&gt;
971-&lt;p&gt;• Fixed @-mentions of other sessions not matching names typed with non-Latin characters (for example Korean entered through an IME)&lt;/p&gt;
972-&lt;p&gt;• Fixed an invalid crossSessionInbound value being silently ignored: it now warns and holds cross-session messages (user settings) or refuses them (managed settings) until fixed&lt;/p&gt;
973-&lt;p&gt;• Fixed rate-limit, usage, and fast-mode messages telling you to run /usage-credits when that command isn't available for your organization (e.g. hidden with DISABLE_EXTRA_USAGE_COMMAND)&lt;/p&gt;
974-&lt;p&gt;• [VSCode] Fixed a chat tab getting stuck on "No conversation found" when its session was never saved; it now starts a new conversation instead&lt;/p&gt;
975-&lt;p&gt;• Improved the Workflow tool's prompt footprint: its description is now about 1k tokens instead of 5.7k, with the script-writing reference moved into a bundled workflow-authoring skill&lt;/p&gt;
976-&lt;p&gt;• Improved the prompt-footer PR badge to check GitHub less often while the pull request is unchanged; a push or a gh pr command still refreshes it right away&lt;/p&gt;
977-&lt;p&gt;• Improved managed settings: client-side timeout, MCP startup-mode, and stream-watchdog env vars no longer trigger the settings-approval prompt&lt;/p&gt;
978-&lt;p&gt;• Improved /ultrareview &amp;lt;PR#&amp;gt; to check before launch that the GitHub account connected to your Claude account can access the repository, and to explain how to fix it, instead of failing after the cloud session starts&lt;/p&gt;
979-&lt;p&gt;• Improved cross-session messaging: falls back to a private per-user /tmp directory when the default one can't be used, and the notice and /status name the directory to fix&lt;/p&gt;
980-&lt;p&gt;• Changed shift+enter in the agent view dispatch input to insert a newline (matching the prompt); ctrl+enter now dispatches and attaches&lt;/p&gt;
981-&lt;p&gt;• Changed /loop: self-paced dynamic mode and the no-prompt autonomous default are now always available, including on Bedrock/Vertex/Foundry&lt;/p&gt;
982-&lt;p&gt;• Changed Anthropic telemetry export failures to log at debug level as [Anthropic telemetry] instead of [3P telemetry] OTEL diag error, so they are not mistaken for your OTel collector failing&lt;/p&gt;
983-&lt;p&gt;• Changed cross-session messaging in Linux user namespaces: root-equivalent trust for unmapped owners is limited to canonical system directories&lt;/p&gt;
984-&lt;p&gt;• Changed SendMessage from a subagent to another session: the result now notes that any reply is delivered to the parent session's conversation, not to the subagent&lt;/p&gt;</content>
985- </entry>
986- <entry>
987- <id>https://github.com/anthropics/claude-code/releases/tag/v2.1.247</id>
988- <title>Claude Code v2.1.247</title>
989- <link rel="alternate" type="text/html" href="https://github.com/anthropics/claude-code/releases/tag/v2.1.247"/>
990- <updated>2026-08-26T23:06:33Z</updated>
991- <content type="html">&lt;p&gt;• Added the SendFeedback tool: when something goes wrong in a session, Claude can draft a feedback report for you to review and send from /feedback (turn off with the feedbackDrafts setting)&lt;/p&gt;
992-&lt;p&gt;• Added {id, text, cooldownSessions, priority} entries, tipsFile, and label to spinnerTipsOverride, so organizations can rotate their own tips alongside the built-in ones&lt;/p&gt;
993-&lt;p&gt;• Added a tip on Bash permission prompts pointing to auto mode, with a one-keystroke "Yes, and switch to auto mode" option&lt;/p&gt;
994-&lt;p&gt;• Added /claude-api cost-optimize to profile an existing project's Claude API spend and work through cost levers (caching, token hygiene, batch, effort, model choice) one measured change at a time&lt;/p&gt;
995-&lt;p&gt;• Updated the /claude-api skill with Admin API coverage (organization members, invites, workspaces, API keys, rate limit reports, workload identity federation, CMEK)&lt;/p&gt;
996-&lt;p&gt;• Fixed fast arrow-key + Enter sequences acting on the row above the one you navigated to in history search, /config, /mcp, /skills, background tasks, and /model&lt;/p&gt;
997-&lt;p&gt;• Fixed sub-agents dying on a first-call model 404: they now use the session's fallback model chain, and the error returned to the parent includes the error type, status, request id, and model&lt;/p&gt;
998-&lt;p&gt;• Fixed a hook or background agent that printed megabytes of error output being able to overflow the conversation and wedge the session on "Prompt is too long"&lt;/p&gt;
999-&lt;p&gt;• Fixed Ctrl keyboard shortcuts not firing under non-Latin (e.g. Cyrillic) keyboard layouts in kitty-protocol terminals&lt;/p&gt;
1000-&lt;p&gt;• Fixed text like &amp;lt;35;150;7M being inserted into the prompt when a mouse report arrived split across reads right after the escape prefix&lt;/p&gt;
1001-&lt;p&gt;• Fixed the Bash sandbox's after-command cleanup deleting a dotfile-managed ~/.claude/settings.json symlink (nix/home-manager, stow) when it is repointed outside the sandbox's writable area&lt;/p&gt;
1002-&lt;p&gt;• Fixed /terminal-setup overwriting your entire Zed keymap.json instead of merging in its keybinding&lt;/p&gt;
1003-&lt;p&gt;• Fixed /rename silently confirming when the session registry could not be updated; it now says other sessions may still show the old name&lt;/p&gt;
1004-&lt;p&gt;• Fixed /compact and "Summarize from here" in sessions started with --agent summarizing under the default system prompt instead of the conversation's own&lt;/p&gt;
1005-&lt;p&gt;• Fixed a background session showing "opening…" forever in claude agents after its terminal host process died; the row now fails within seconds with the reason, and Enter restarts it&lt;/p&gt;
1006-&lt;p&gt;• Fixed unbounded memory growth when a hook's or background task's output file could not be written; the file now notes where output was lost&lt;/p&gt;
1007-&lt;p&gt;• Fixed /install-github-app over SSH: the copy shortcut now says how the sign-in URL was copied instead of always claiming success, and the URL appears immediately when no browser can open&lt;/p&gt;
1008-&lt;p&gt;• Fixed shell commands carried over from the foreground logging an internal error or showing a misleading [exited with code -1] line when they finish in background sessions&lt;/p&gt;
1009-&lt;p&gt;• Fixed a version-less marketplace plugin's live cache directory being deleted and recreated on a second-scope install, which could disrupt a running session using it&lt;/p&gt;
1010-&lt;p&gt;• Fixed Remote Control sessions started with /remote-control not reporting the working-tree diff to connected clients&lt;/p&gt;
1011-&lt;p&gt;• Fixed self-hosted runner sessions reporting running before Claude Code had started, which could trigger a premature "Claude is waiting for your input" notification from the Claude desktop app&lt;/p&gt;
1012-&lt;p&gt;• Fixed first-run setup exiting with "Unable to connect to Anthropic services" when managed settings configure Claude apps gateway sign-in and Anthropic endpoints are unreachable&lt;/p&gt;
1013-&lt;p&gt;• Fixed cloud sessions (Claude Code on the web, desktop and mobile apps) sometimes showing the previous permission mode when you switch modes right after sending a message&lt;/p&gt;
1014-&lt;p&gt;• Fixed cloud sessions going silent when the session's container restarts between turns while a background agent, shell, or monitor is still running — the resumed session now reports the lost work&lt;/p&gt;
1015-&lt;p&gt;• Improved plugin marketplace hardening: names containing control or invisible characters are rejected, and marketplace-supplied text in /plugin and claude plugin output is escape-safe&lt;/p&gt;
1016-&lt;p&gt;• Improved Bedrock, Vertex, and Foundry sessions (and any with telemetry disabled): Claude is now told when a configured MCP server failed to connect, instead of concluding its tools don't exist&lt;/p&gt;
1017-&lt;p&gt;• Changed Sonnet 5's default auto-compact window to its full 1M context, so sessions on the 1M window now auto-compact at about 967K tokens instead of about 934K&lt;/p&gt;
1018-&lt;p&gt;• Changed cross-session peer messages to collapse by default to a one-line Message from @&amp;lt;sender&amp;gt;: &amp;lt;first line&amp;gt; preview; Ctrl+O expands the full body&lt;/p&gt;
1019-&lt;p&gt;• Changed terminal hyperlinks in rendered markdown: link targets that point at a network or automounter path, contain a control character, or lead with an invisible character now render as plain text&lt;/p&gt;
1020-&lt;p&gt;• Changed the prompt-footer PR badge to skip its GitHub re-check on terminal refocus when the last check is under a minute old&lt;/p&gt;
1021-&lt;p&gt;• Changed analytics to stay off from startup, not only after login, when managed settings force gateway login or a custom OAuth deployment is configured&lt;/p&gt;
1022-&lt;p&gt;• Changed Claude apps gateway sign-in requests to identify Claude Code (a surface=claude_code device-authorization parameter and a claude-code/&amp;lt;version&amp;gt; User-Agent)&lt;/p&gt;
1023-&lt;p&gt;• Changed organization sign-in enforcement to exit at start when the administrator's managed settings cannot be read, even if host-supplied or per-user Windows registry settings exist&lt;/p&gt;</content>
1024- </entry>
10251163 </feed>
@@ -2,7 +2,7 @@
22
33 A mod is a Claude Code plugin whose behaviour lives in a hooks module: one
44 `register(on, options)` entry that hooks the engine's events as functions
5-`($, e, next)`. These three ship inside Claude Code; this folder is their
5+`($, e, next)`. These four ship inside Claude Code; this folder is their
66 source, published as it is built into the binary.
77
88 | Mod | What it does | Seated |
@@ -10,6 +10,7 @@
1010 | [`sec-default`](sec-default) | Keeps an organization's classic hooks, prompt content, managed settings and tool policy out of reach of the plugins a person installs; adds no policy of its own. | Outermost, on a machine with managed settings or for a Team or Enterprise organization, unless managed `prependPlugins` says otherwise |
1111 | [`diff`](diff) | `/diff`: the session's uncommitted changes in a pane beside the transcript, file by file with their hunks, refreshed as Claude edits files and runs commands. | Built in |
1212 | [`telemetry`](telemetry) | Adds `$.telemetry` (`log`, `mark`) in the `engine.create` fold so a plugin can record an event as a first-party analytics row; sends nothing wherever Claude Code's analytics are off. | Built in |
13+| [`agents-md`](agents-md) | `AGENTS.md` as project instructions, by one option: loaded where the project has no `CLAUDE.md` of its own (`claude-md-or-agents-md`, the default) or beside it (`claude-md-and-agents-md`), placed and framed exactly as the engine places `CLAUDE.md`, nested ones on a `Read`; or the project's and the person's instruction files dropped and the organization's kept (`managed-only`); or `CLAUDE.md` alone, as the engine reads it (`claude-md`). | Built in |
1314
1415 Each folder is a complete plugin: `.claude-plugin/plugin.json`, a
1516 `hooks/hooks.json` naming the module, and TypeScript under `hooks/` typed
@@ -1,0 +1,23 @@
1+{
2+ "name": "agents-md",
3+ "version": "0.1.0",
4+ "description": "AGENTS.md as project instructions, by one option. Under claude-md-or-agents-md a project with no instruction files of its own gets its AGENTS.md files instead: a prompt.context hook asks $.fs.ancestors for every AGENTS.md and .claude/AGENTS.md from the filesystem root down to the working directory and hands them to the engine as project instruction files, which it renders, announces and withholds from agents exactly as it does CLAUDE.md, skipping any file the engine already loaded by import or link; a tool.call hook on Read attaches the ones under the project root as the engine attaches a nested CLAUDE.md. Under claude-md-and-agents-md every AGENTS.md is loaded beside CLAUDE.md. Under managed-only the project's and the person's instruction files are dropped and the organization's managed ones kept. Under claude-md the engine's walk stands alone. claude-md-or-agents-md is the default.",
5+ "author": {
6+ "name": "Anthropic"
7+ },
8+ "userConfig": {
9+ "instructionFiles": {
10+ "type": "string",
11+ "title": "Project instructions",
12+ "description": "\"claude-md\": CLAUDE.md only, loaded by the engine as today. \"claude-md-or-agents-md\" (default): a project with no CLAUDE.md of its own gets its AGENTS.md files instead, loaded exactly where and how CLAUDE.md would be. \"claude-md-and-agents-md\": AGENTS.md files are loaded beside CLAUDE.md (a file CLAUDE.md already imports or links to is not loaded twice). \"managed-only\": the project's and your own instruction files are dropped; the organization's managed CLAUDE.md and memory stay.",
13+ "required": false,
14+ "default": "claude-md-or-agents-md",
15+ "options": [
16+ "claude-md",
17+ "claude-md-or-agents-md",
18+ "claude-md-and-agents-md",
19+ "managed-only"
20+ ]
21+ }
22+ }
23+}
@@ -1,0 +1,169 @@
1+# agents-md
2+
3+`AGENTS.md` read the way Claude Code reads `CLAUDE.md`, as a plugin, under
4+one option, `instructionFiles`:
5+
6+- `claude-md`: only `CLAUDE.md` is loaded, by the engine, as today. The plugin
7+ adds nothing.
8+- `claude-md-or-agents-md` (the default): a project with no instruction files
9+ of its own gets its `AGENTS.md` files instead, loaded exactly where and how
10+ `CLAUDE.md` would be. "Of its own" is read off what the engine loaded for
11+ the context: a `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md` in any
12+ directory from the root down to the working directory leaves the whole
13+ project to the engine, and the plugin stays out (the organization's managed
14+ file, the person's `~/.claude/CLAUDE.md`, a `.claude/rules` file and an
15+ added directory's `CLAUDE.md` do not count, as the nested walk does not see
16+ them either). With none, every `AGENTS.md` and `.claude/AGENTS.md` on that
17+ path joins the instruction files the engine renders, and a `Read` under a
18+ subdirectory attaches that directory's `AGENTS.md` unless a `CLAUDE.md`
19+ there claims it.
20+- `claude-md-and-agents-md`: every `AGENTS.md` is loaded beside `CLAUDE.md`,
21+ up and down the tree; a file `CLAUDE.md` already `@`-imports, or is a link
22+ to, is not loaded a second time (compared by path, then by content).
23+- `managed-only`: the project's checked-in and private instruction files and
24+ the person's own are dropped from the context; the organization's managed
25+ `CLAUDE.md` and the engine's memory stay. The engine's nested `CLAUDE.md`
26+ attachments on `Read` are not an event yet and still arrive. (The engine's
27+ `claudeMdExcludes` setting also exists, for user, project and local files,
28+ and applies to the `AGENTS.md` files this plugin reads too.)
29+
30+How the files reach the model is the engine's doing, not the plugin's:
31+`prompt.context` hands a hook the instruction files behind `claudeMd`
32+(`{ path, kind, content, parent? }`, kinds `managed`, `user`, `project`,
33+`local`, `memory`, in load order) and a hook answers the list changed. The
34+engine then renders `claudeMd` from the answered files with its own preamble
35+and framing, announces them by name, and keeps only the `managed` ones for an
36+agent that omits project instructions (Explore, Plan, a custom agent with
37+`omitClaudeMd`). So an `AGENTS.md` this plugin adds as a `project` file is, to
38+everything downstream, a project instruction file: same place in the context,
39+same framing, same omission rules, same announcement. An organization's
40+prepended plugin on `prompt.context` sits above this one and has the last
41+word on the files.
42+
43+`hooks/register.ts` is the module; everything under `hooks/` is its parts,
44+importing `claude-code` and one another alone. `tests/` runs under
45+`claude plugin test <this folder>`.
46+
47+## Setting the option
48+
49+As a built-in its option is the `/config` row "Project instructions", a
50+picker over the four values, each described there. By hand it is
51+
52+```json
53+{
54+ "pluginConfigs": {
55+ "agents-md@builtin": {
56+ "options": { "instructionFiles": "claude-md-and-agents-md" }
57+ }
58+ }
59+}
60+```
61+
62+in user settings (`~/.claude/settings.json`), `--settings`, or managed
63+settings; a project's `.claude/settings.json` is not read for plugin
64+options. Changing it reloads the module, and the next context the engine
65+builds (the next turn after the reload, a new conversation, `/clear`, a
66+compaction) carries the new mode's files. A hand-typed value outside the
67+four is told once in the transcript and reads as the default. `/plugin`
68+lists the plugin among the built-ins, where a person can turn it off; with
69+it off the engine reads `CLAUDE.md` alone. No hooks setting or CLI mode turns
70+it off (`disableAllHooks`, `allowManagedHooksOnly` and `--bare` govern
71+settings hooks and installed plugins, not built-ins); where the engine loads
72+no instruction files (`--bare` without `--add-dir`, `--safe-mode`,
73+`CLAUDE_CODE_DISABLE_CLAUDE_MDS`) its walk finds none and it adds none,
74+`CLAUDE.md` and `AGENTS.md` alike.
75+
76+The option was first keyed `projectInstructions`, with the values `claude`,
77+`agents-fallback`, `both` and `none`. A value still stored under that key is
78+honoured for now while `instructionFiles` reads as its default: `none` as
79+`managed-only`, `claude` as `claude-md`, `agents-fallback` as
80+`claude-md-or-agents-md`, `both` as `claude-md-and-agents-md`, any other
81+value as `claude-md` (which adds nothing, never as the default, which loads
82+`AGENTS.md`); the first `session.start` of a load says in the transcript how
83+it was read. Once `instructionFiles` is set to anything but its default, the
84+old key is not read and the transcript says to remove it.
85+
86+Run from this folder instead (`claude --plugin-dir mods/agents-md`), the
87+same entry is keyed `"agents-md"`.
88+
89+## What it hooks
90+
91+| event | what the hook does |
92+| --- | --- |
93+| `session.start` | in every mode: passes the start straight through and floats the usage row for the configured mode, never awaited; the first start of a load logs how a stored `projectInstructions` value is read. The session's start never waits on this plugin |
94+| `prompt.context` | under `claude-md-or-agents-md` and `claude-md-and-agents-md`: walks `$.fs.ancestors` for the `AGENTS.md` files above the working directory and answers them as `project` instruction files, each `@` import its own entry after its file, each placed where a project file of its directory stands (root first, before the first deeper project file, else after the last project file, before memory); files the engine already holds by path or by content are left out; under `claude-md-or-agents-md` it answers nothing when the project has a `CLAUDE.md` of its own (among the handed files, else found by a `$.fs.ancestors` walk, so a `CLAUDE.md` the engine loaded and then withheld still counts), and logs which files it loaded once, and again after a move to another project root; handed unknown files (a hook above rewrote the `claudeMd` text) it adds nothing; the first context of a load sends the load row (counts) and the feature mark; under `managed-only` (matcher: a `project`, `local` or `user` file present): answers the list without those kinds |
95+| `agent.spawn` on `fork: true` | under `claude-md-or-agents-md` and `claude-md-and-agents-md`: a fork the Agent tool starts shares its parent's prompt prefix, so the parent loop's delivered nested files are copied to the fork's loop and not attached to it again (a `/fork` or `/subtask` fork does not raise `agent.spawn` yet and starts from an empty set, as every fork did before; a fork started in the same tool batch as a `Read` inherits that Read's file although its prefix holds a placeholder for it) |
96+| `tool.call` on `Read` | under `claude-md-or-agents-md` and `claude-md-and-agents-md`, for a file under the session's project root (`$.session.root()`, read live, so `/cd`, a host's directory change and worktree moves are followed and a moved root starts the delivered sets and the fallback decision over; a file elsewhere gets nothing, as the engine attaches no nested `CLAUDE.md` there; and nothing anywhere in a run where the engine attaches nothing to a turn, `--bare` with its `CLAUDE_CODE_SIMPLE` or `CLAUDE_CODE_DISABLE_ATTACHMENTS`, read on every Read through `$.env.get` as the engine reads them on every turn): walks only the directories strictly between the root and the read file (`$.fs.ancestors` with `below: root`, as the engine walks only those for a nested `CLAUDE.md`, never up to the filesystem root again) and attaches their `AGENTS.md` files not yet given to that agent loop, not already among the context's instruction files (by path or, for a project file, by text) and not claimed by a `CLAUDE.md` of the same directory (or imported by one), as `context` after the tool result, framed `Contents of <path>:` byte for byte as the engine frames a nested `CLAUDE.md`, whatever its size; each file once per loop and conversation (the context's recomputation after a compaction or `/clear` starts the count over), the context's files never; a Read that attached files sends the nested row. A `~` or `~/` path is read under the home directory as the Read tool reads it |
97+
98+## What it calls on `$`
99+
100+`fs.ancestors` (with each found file's `parts`: the file and its imports
101+apart; with `below` on a Read; it finds nothing on a thin client, whose
102+workspace files are remote, as the engine's own walk does), `session.root`,
103+`session.cwd`, `env.get` (`HOME` and `USERPROFILE`, once per load, the
104+profile first on a Windows spelling of the working directory, so a `~/` path
105+the model hands a Read resolves where the Read tool reads it; `CLAUDE_CODE_SIMPLE`
106+and `CLAUDE_CODE_DISABLE_ATTACHMENTS` on every Read), `ui.log`,
107+`telemetry.log` and `telemetry.mark`.
108+
109+`$.telemetry` is the [telemetry](../telemetry) plugin's noun; where that
110+plugin is not seated the calls find no noun and are dropped without a trace,
111+and nothing else changes.
112+
113+## What it logs
114+
115+Counts and closed choices only; no path and no file text. Each row goes
116+through `$.telemetry.log`, so it exists only where the telemetry plugin
117+does:
118+
119+| event | when | properties |
120+| --- | --- | --- |
121+| `agents_md_mode` | once per fresh load, at `session.start` | `mode` (`claude-md` \| `claude-md-or-agents-md` \| `claude-md-and-agents-md` \| `managed-only`), `is_interactive` |
122+| `agents_md_load` | the first context of a load, under `claude-md-or-agents-md` and `claude-md-and-agents-md` | `mode`, `file_count` (`AGENTS.md` files handed to the engine), `import_count` (their `@` imports), `total_content_length`, `yielded` (`claude-md-or-agents-md` stood down for a `CLAUDE.md` of the project's own), `walk_failed`; with it one `$.telemetry.mark` for feature `agents_md`: `ok`, or `sad` with reason `walk_failed` |
123+| `agents_md_nested` | a Read that attached nested files | `mode`, `file_count` |
124+
125+## Where it still differs from CLAUDE.md
126+
127+All of these apply only to the modes that load `AGENTS.md`,
128+`claude-md-or-agents-md` (the default) and `claude-md-and-agents-md`. Each
129+names a loader fact a plugin cannot reach through the events it has today.
130+
131+1. Nested files attach on a text `Read` only. The engine also attaches a
132+ directory's `CLAUDE.md` for a file `@`-mentioned in the prompt, for the
133+ IDE's opened file or selection, and for the `Read` tool's notebook, image
134+ and PDF results.
135+2. A nested file the plugin attaches is not registered in the loop's
136+ read-file state, so after a compaction the engine does not restore it
137+ among the recently read files (the plugin attaches it again at the next
138+ `Read` under that directory instead), and a change to it mid-session is
139+ not re-announced.
140+3. `/cd` carries the new tree's `CLAUDE.md` in its own notice; the plugin's
141+ files for the new tree arrive in the same next request through the
142+ engine's instructions announcement instead.
143+4. Paths compare by spelling; the engine resolves a symlinked alias of the
144+ working directory before deciding a file is inside it.
145+5. `--add-dir` directories contribute no `AGENTS.md`, where the engine can
146+ load their `CLAUDE.md`.
147+6. `/memory` and the `#` shortcut do not know `AGENTS.md` files, and the
148+ engine's own initial-load row does not count them (this plugin's
149+ `agents_md_load` row does).
150+7. An `@` import outside the working directory inside an `AGENTS.md` is
151+ honoured only once the approval the engine asks for a `CLAUDE.md`'s
152+ external imports has been given (without it the import is left out, as a
153+ `CLAUDE.md`'s is); the approval dialog itself is raised for `CLAUDE.md`
154+ imports alone.
155+8. A subagent that is not a fork gets a nested `AGENTS.md` at its own first
156+ `Read` under that directory even when its parent's loop was already given
157+ it; the engine does not hand such a subagent the nested `CLAUDE.md` again.
158+ A fork matches the engine on both sides.
159+
160+## Testing
161+
162+ claude plugin test mods/agents-md
163+
164+`tests/register.test.ts` covers the default mode: a project with `AGENTS.md`
165+alone gets it as a project instruction file and one transcript line naming
166+it, a project with a `CLAUDE.md` of its own is left to the engine without a
167+walk, a failed walk leaves the context as handed, and the start hands
168+`$.telemetry` the mode row alone where a test seats a provider for that
169+noun, and goes on untouched where none is seated.
@@ -1,0 +1,28 @@
1+import type { InstructionFile } from 'claude-code'
2+
3+/**
4+ * The path of the file at the head of an instruction file's `@`-import
5+ * chain; the file's own path when nothing imported it.
6+ *
7+ * Follows `parent` among the given files until a file nothing imported.
8+ *
9+ * @param file the instruction file
10+ * @param byPath the files it may have come through, by path
11+ * @returns the chain head's path
12+ */
13+export function chainRootOf(
14+ file: InstructionFile,
15+ byPath: ReadonlyMap<string, InstructionFile>,
16+): string {
17+ const seen = new Set<string>([file.path])
18+ let path = file.path
19+ let parent = file.parent
20+
21+ while (parent !== undefined && !seen.has(parent)) {
22+ seen.add(parent)
23+ path = parent
24+ parent = byPath.get(parent)?.parent
25+ }
26+
27+ return path
28+}
@@ -1,0 +1,12 @@
1+import type { InstructionFileKind } from 'claude-code'
2+
3+/**
4+ * The kinds `managed-only` drops: the project's checked-in and private
5+ * instruction files and the person's own; what it keeps is the organization's
6+ * and memory.
7+ */
8+export const DROPPED_KINDS: readonly InstructionFileKind[] = [
9+ 'project',
10+ 'local',
11+ 'user',
12+]
@@ -1,0 +1,20 @@
1+import type { FsAncestor, InstructionFile } from 'claude-code'
2+
3+/**
4+ * The found AGENTS.md files as project instruction files, one per file and
5+ * per file it `@`-imported, in walk and load order.
6+ *
7+ * An import names the file that brought it as its parent.
8+ *
9+ * @param found what `$.fs.ancestors` found, root first
10+ * @returns the instruction files, kind `project`
11+ */
12+export const filesOf = (found: readonly FsAncestor[]): InstructionFile[] =>
13+ found.flatMap(entry =>
14+ entry.parts.map((part, index) => ({
15+ path: part.path,
16+ kind: 'project' as const,
17+ content: part.content,
18+ ...(index > 0 && { parent: entry.parts[0]?.path ?? part.path }),
19+ })),
20+ )
@@ -1,0 +1,12 @@
1+export * from './chain-root-of.js'
2+export * from './dropped-kinds.js'
3+export * from './files-of.js'
4+export * from './insertion-index.js'
5+export * from './is-claude-file-on-walk.js'
6+export * from './is-kept-without-instructions.js'
7+export * from './is-project-own.js'
8+export * from './project-dir-of.js'
9+export * from './unseen-files.js'
10+export * from './with-project-files.js'
11+
12+export * as default from '.'
@@ -1,0 +1,44 @@
1+import type { InstructionFile } from 'claude-code'
2+
3+import Frames from '../frames'
4+import { chainRootOf } from './chain-root-of.js'
5+import { isProjectOwn } from './is-project-own.js'
6+import { projectDirOf } from './project-dir-of.js'
7+
8+/**
9+ * Where a directory's AGENTS.md files go among the files the engine loaded,
10+ * as the engine orders project files root first.
11+ *
12+ * Before the first project file of a deeper directory (an imported file
13+ * counts with the file at the head of its chain), else after the last
14+ * project file, else before the engine's memory, else at the end.
15+ *
16+ * @param files the list so far
17+ * @param dir the directory the AGENTS.md files stand in
18+ * @returns the index to insert at
19+ */
20+export function insertionIndex(
21+ files: readonly InstructionFile[],
22+ dir: string,
23+): number {
24+ const byPath = new Map(files.map(file => [file.path, file]))
25+ const deeper = files.findIndex(
26+ file =>
27+ isProjectOwn(file) &&
28+ Frames.isBelow(projectDirOf(chainRootOf(file, byPath)), dir),
29+ )
30+
31+ if (deeper !== -1) {
32+ return deeper
33+ }
34+
35+ const lastOwn = files.findLastIndex(isProjectOwn)
36+
37+ if (lastOwn !== -1) {
38+ return lastOwn + 1
39+ }
40+
41+ const memory = files.findIndex(file => file.kind === 'memory')
42+
43+ return memory === -1 ? files.length : memory
44+}
@@ -1,0 +1,39 @@
1+import type { InstructionFile } from 'claude-code'
2+
3+import Frames from '../frames'
4+import Names from '../names'
5+import { isProjectOwn } from './is-project-own.js'
6+import { projectDirOf } from './project-dir-of.js'
7+
8+/**
9+ * Whether a handed instruction file is a CLAUDE.md, `.claude/CLAUDE.md` or
10+ * CLAUDE.local.md of a directory on the walk down to the session's root.
11+ *
12+ * What makes a project "have a CLAUDE.md of its own" for
13+ * `claude-md-or-agents-md`, the same files the Read walk asks for; a rules
14+ * file, an imported file or an added directory's CLAUDE.md is not one.
15+ *
16+ * @param file the handed instruction file
17+ * @param root the session's project root, absolute
18+ * @returns true for the project's own CLAUDE.md files on the walk
19+ */
20+export function isClaudeFileOnWalk(
21+ file: InstructionFile,
22+ root: string,
23+): boolean {
24+ const isOwnClaudeFile =
25+ isProjectOwn(file) &&
26+ file.parent === undefined &&
27+ Names.CLAUDE_NAMES.some(name =>
28+ Frames.normalSpellingOf(file.path).endsWith(`/${name}`),
29+ )
30+
31+ if (!isOwnClaudeFile) {
32+ return false
33+ }
34+
35+ const dir = projectDirOf(file.path)
36+ const spelledRoot = Frames.normalSpellingOf(root)
37+
38+ return dir === spelledRoot || Frames.isBelow(spelledRoot, dir)
39+}
@@ -1,0 +1,13 @@
1+import type { InstructionFile } from 'claude-code'
2+
3+import { DROPPED_KINDS } from './dropped-kinds.js'
4+
5+/**
6+ * Whether `managed-only` keeps an instruction file: the organization's managed
7+ * files and the engine's memory stay, the project's and the person's go.
8+ *
9+ * @param file the instruction file
10+ * @returns true for a file `managed-only` leaves in place
11+ */
12+export const isKeptWithoutInstructions = (file: InstructionFile): boolean =>
13+ !DROPPED_KINDS.includes(file.kind)
@@ -1,0 +1,11 @@
1+import type { InstructionFile } from 'claude-code'
2+
3+/**
4+ * Whether an instruction file is one of the project's own (checked in or
5+ * private), as opposed to the organization's, the person's or memory.
6+ *
7+ * @param file the instruction file
8+ * @returns true for a `project` or `local` file
9+ */
10+export const isProjectOwn = (file: InstructionFile): boolean =>
11+ file.kind === 'project' || file.kind === 'local'
@@ -1,0 +1,16 @@
1+import Frames from '../frames'
2+
3+/**
4+ * The project directory an instruction file speaks for: the directory
5+ * holding it, or the one holding its `.claude` folder.
6+ *
7+ * @param path the file's path, absolute
8+ * @returns the directory, in the walk's normal spelling
9+ */
10+export function projectDirOf(path: string): string {
11+ const spelled = Frames.normalSpellingOf(path)
12+ const claudeAt = spelled.lastIndexOf('/.claude/')
13+ const cut = claudeAt === -1 ? spelled.lastIndexOf('/') : claudeAt
14+
15+ return cut <= 0 ? '/' : spelled.slice(0, cut)
16+}
@@ -1,0 +1,40 @@
1+import type { InstructionFile } from 'claude-code'
2+
3+import Frames from '../frames'
4+import { isProjectOwn } from './is-project-own.js'
5+
6+/**
7+ * The candidate files the engine has not already loaded, and no candidate
8+ * twice.
9+ *
10+ * Leaves out one whose path is among the handed files (a CLAUDE.md that
11+ * `@`-imports AGENTS.md) or whose text equals a handed project file's (a
12+ * CLAUDE.md symlinked to AGENTS.md); other kinds' text does not count.
13+ *
14+ * @param candidates the AGENTS.md instruction files of a walk
15+ * @param handed the files the engine handed the hook
16+ * @returns the candidates worth adding, in order
17+ */
18+export function unseenFiles(
19+ candidates: readonly InstructionFile[],
20+ handed: readonly InstructionFile[],
21+): InstructionFile[] {
22+ const paths = new Set(handed.map(file => Frames.normalSpellingOf(file.path)))
23+ const texts = new Set(
24+ handed.filter(isProjectOwn).map(file => file.content.trim()),
25+ )
26+ const kept: InstructionFile[] = []
27+
28+ for (const file of candidates) {
29+ const path = Frames.normalSpellingOf(file.path)
30+ const text = file.content.trim()
31+ const isSeen = paths.has(path) || (text !== '' && texts.has(text))
32+
33+ if (!isSeen) {
34+ paths.add(path)
35+ kept.push(file)
36+ }
37+ }
38+
39+ return kept
40+}
@@ -1,0 +1,37 @@
1+import type { InstructionFile } from 'claude-code'
2+
3+import { chainRootOf } from './chain-root-of.js'
4+import { insertionIndex } from './insertion-index.js'
5+import { projectDirOf } from './project-dir-of.js'
6+
7+/**
8+ * The engine's instruction files with the AGENTS.md files placed where the
9+ * engine would place a project file of their directory; as is when none.
10+ *
11+ * @param handed the files the engine handed the hook, in its order
12+ * @param added the AGENTS.md instruction files to add, root first
13+ * @returns the merged list
14+ */
15+export function withProjectFiles(
16+ handed: readonly InstructionFile[],
17+ added: readonly InstructionFile[],
18+): readonly InstructionFile[] {
19+ if (added.length === 0) {
20+ return handed
21+ }
22+
23+ const merged = [...handed]
24+ const byPath = new Map(added.map(file => [file.path, file]))
25+ const byDir = new Map<string, InstructionFile[]>()
26+
27+ for (const file of added) {
28+ const dir = projectDirOf(chainRootOf(file, byPath))
29+ byDir.set(dir, [...(byDir.get(dir) ?? []), file])
30+ }
31+
32+ for (const [dir, files] of byDir) {
33+ merged.splice(insertionIndex(merged, dir), 0, ...files)
34+ }
35+
36+ return merged
37+}
@@ -1,0 +1,33 @@
1+/**
2+ * A path the model handed a Read, made absolute as the Read tool reads it.
3+ *
4+ * As given when it is absolute; under the home directory (less any trailing
5+ * separator) for `~` or a `~/` spelling when a home is known; else under the
6+ * working directory.
7+ *
8+ * @param path the Read's `file_path` as the model spelled it
9+ * @param root the session's working directory, absolute
10+ * @param home the home directory, absolute, when the environment names one
11+ * @returns the absolute path
12+ */
13+export function absoluteOf(path: string, root: string, home?: string): string {
14+ const isHomeSpelling = path === '~' || path.startsWith('~/')
15+
16+ if (isHomeSpelling && home !== undefined) {
17+ const bare = home.replace(/(?<=.)[\\/]+$/, '')
18+
19+ return path === '~' ? bare : `${bare}${separatorOf(bare)}${path.slice(2)}`
20+ }
21+
22+ return /^(?:[A-Za-z]:)?[\\/]/.test(path)
23+ ? path
24+ : `${root}${separatorOf(root)}${path}`
25+}
26+
27+/**
28+ * The separator a directory's own spelling uses.
29+ *
30+ * @param dir an absolute directory
31+ * @returns `\` for a backslash spelling, `/` otherwise
32+ */
33+const separatorOf = (dir: string): string => (dir.includes('\\') ? '\\' : '/')
@@ -1,0 +1,8 @@
1+export * from './absolute-of.js'
2+export * from './is-below.js'
3+export * from './is-file-at.js'
4+export * from './nested-frame.js'
5+export * from './normal-spelling-of.js'
6+export * from './outside-claude-dirs.js'
7+
8+export * as default from '.'
@@ -1,0 +1,12 @@
1+import { normalSpellingOf } from './normal-spelling-of.js'
2+
3+/**
4+ * Whether a path lies strictly inside a directory, by spelling (no links
5+ * followed), as the engine bounds its nested CLAUDE.md walk.
6+ *
7+ * @param path an absolute path
8+ * @param dir an absolute directory
9+ * @returns true when `path` is under `dir` and not `dir` itself
10+ */
11+export const isBelow = (path: string, dir: string): boolean =>
12+ normalSpellingOf(path).startsWith(`${normalSpellingOf(dir)}/`)
@@ -1,0 +1,14 @@
1+import type { InstructionFile } from 'claude-code'
2+
3+import { normalSpellingOf } from './normal-spelling-of.js'
4+
5+/**
6+ * Whether an instruction file is the file at a path, however either is
7+ * spelled.
8+ *
9+ * @param file the instruction file
10+ * @param path an absolute path
11+ * @returns true when both name one path
12+ */
13+export const isFileAt = (file: InstructionFile, path: string): boolean =>
14+ normalSpellingOf(file.path) === normalSpellingOf(path)
@@ -1,0 +1,11 @@
1+import type { InstructionFile } from 'claude-code'
2+
3+/**
4+ * An instruction file as a Read attaches it, framed as the engine frames a
5+ * nested CLAUDE.md: the text as loaded, trailing newline and all.
6+ *
7+ * @param file the file
8+ * @returns the `Contents of <path>` frame over its text
9+ */
10+export const nestedFrame = (file: InstructionFile): string =>
11+ `Contents of ${file.path}:\n\n${file.content}`
@@ -1,0 +1,9 @@
1+/**
2+ * A path with one separator spelling and no trailing separator, so two
3+ * spellings of one path compare equal (`C:/repo/x` and `C:\repo\x`).
4+ *
5+ * @param path an absolute path as a walk or the model spelled it
6+ * @returns the path with `/` for every separator, no trailing one
7+ */
8+export const normalSpellingOf = (path: string): string =>
9+ path.replaceAll('\\', '/').replace(/(?<=.)\/+$/, '')
@@ -1,0 +1,21 @@
1+import type { FsAncestor } from 'claude-code'
2+
3+/**
4+ * The AGENTS.md files standing in a directory with no CLAUDE.md of its own,
5+ * in walk order.
6+ *
7+ * Where the engine attaches a directory's CLAUDE.md, that directory's
8+ * AGENTS.md is left out rather than merged.
9+ *
10+ * @param agents the AGENTS.md files of a walk, root first
11+ * @param claude the CLAUDE.md files of the same walk
12+ * @returns the AGENTS.md files whose directory holds no CLAUDE.md
13+ */
14+export function outsideClaudeDirs(
15+ agents: readonly FsAncestor[],
16+ claude: readonly FsAncestor[],
17+): readonly FsAncestor[] {
18+ const taken = new Set(claude.map(file => file.dir))
19+
20+ return agents.filter(file => !taken.has(file.dir))
21+}
@@ -1,0 +1,4 @@
1+{
2+ "description": "AGENTS.md up and down the tree like CLAUDE.md, by the instructionFiles option: under claude-md-or-agents-md (the default; where the project has no CLAUDE.md of its own on the walk) or claude-md-and-agents-md, a prompt.context hook hands every ancestor AGENTS.md to the engine among its instruction files, skipping ones it already loaded, and a tool.call hook on Read attaches the AGENTS.md files of the directories between the project root and a read file, once a conversation and agent loop (an Agent-tool fork inherits its parent's), except in a run where the engine attaches nothing to a turn (--bare, CLAUDE_CODE_DISABLE_ATTACHMENTS); under managed-only, a prompt.context hook keeps only the organization's managed files and memory; under claude-md, nothing is added; in every mode a session.start hook sends the plugin's usage row through $.telemetry where that noun is seated",
3+ "modules": ["./register.ts"]
4+}
@@ -1,0 +1,9 @@
1+export * from './files'
2+export * from './frames'
3+export * from './modes'
4+export * from './names'
5+export * from './register.js'
6+export * from './switches'
7+export * from './telemetry'
8+
9+export * as default from '.'
@@ -1,0 +1,8 @@
1+import type { Mode } from './types'
2+
3+/**
4+ * The mode an unset option reads as: AGENTS.md where the project has no
5+ * CLAUDE.md of its own, the engine's CLAUDE.md walk standing alone where it
6+ * has one.
7+ */
8+export const DEFAULT_MODE: Mode = 'claude-md-or-agents-md'
@@ -1,0 +1,7 @@
1+export * from './default-mode.js'
2+export * from './legacy-mode-of.js'
3+export * from './mode-of.js'
4+export * from './modes.js'
5+export * from './types'
6+
7+export * as default from '.'
@@ -1,0 +1,33 @@
1+import type { Mode } from './types'
2+
3+/**
4+ * Each value the option took under its old key, `projectInstructions`, and
5+ * the mode that keeps its meaning.
6+ */
7+const LEGACY_MODES: Readonly<Record<string, Mode>> = {
8+ none: 'managed-only',
9+ claude: 'claude-md',
10+ 'agents-fallback': 'claude-md-or-agents-md',
11+ both: 'claude-md-and-agents-md',
12+}
13+
14+/**
15+ * The mode a value stored under the old `projectInstructions` key keeps; a
16+ * value it never took reads as `claude-md`, which adds nothing, never as the
17+ * default, which loads AGENTS.md.
18+ *
19+ * COMPAT_BREAK(agents-md-project-instructions): drop the projectInstructions
20+ * mapping once stored settings have migrated.
21+ *
22+ * @param value the `projectInstructions` option as stored
23+ * @returns the mode it keeps, undefined when nothing is stored there
24+ */
25+export function legacyModeOf(value: unknown): Mode | undefined {
26+ if (value === undefined) {
27+ return undefined
28+ }
29+
30+ return (
31+ (typeof value === 'string' ? LEGACY_MODES[value] : undefined) ?? 'claude-md'
32+ )
33+}
@@ -1,0 +1,13 @@
1+import { DEFAULT_MODE } from './default-mode.js'
2+import { MODES } from './modes.js'
3+import type { Mode } from './types'
4+
5+/**
6+ * The mode a stored option names; unset, a typo or a value of another type
7+ * names the default.
8+ *
9+ * @param value the `instructionFiles` option as stored
10+ * @returns the mode named, DEFAULT_MODE for anything else
11+ */
12+export const modeOf = (value: unknown): Mode =>
13+ MODES.find(mode => mode === value) ?? DEFAULT_MODE
@@ -1,0 +1,11 @@
1+import type { Mode } from './types'
2+
3+/**
4+ * Every mode, in the order the option's description names them.
5+ */
6+export const MODES: readonly Mode[] = [
7+ 'claude-md',
8+ 'claude-md-or-agents-md',
9+ 'claude-md-and-agents-md',
10+ 'managed-only',
11+]
@@ -1,0 +1,3 @@
1+export * from './mode'
2+
3+export * as default from '.'
@@ -1,0 +1,3 @@
1+export type * from './mode.js'
2+
3+export * as default from '.'
@@ -1,0 +1,15 @@
1+/**
2+ * Which project instructions the plugin loads: the `instructionFiles`
3+ * option.
4+ *
5+ * `claude-md` loads none of its own, the engine's CLAUDE.md walk standing
6+ * alone; `claude-md-or-agents-md` loads AGENTS.md where the project has no
7+ * CLAUDE.md; `claude-md-and-agents-md` loads AGENTS.md beside CLAUDE.md;
8+ * `managed-only` loads neither, taking the project's and the person's
9+ * CLAUDE.md files out of the conversation's context.
10+ */
11+export type Mode =
12+ | 'claude-md'
13+ | 'claude-md-or-agents-md'
14+ | 'claude-md-and-agents-md'
15+ | 'managed-only'
@@ -1,0 +1,5 @@
1+/**
2+ * The instruction files read in each directory, in the order the engine
3+ * reads CLAUDE.md and .claude/CLAUDE.md.
4+ */
5+export const AGENTS_NAMES = ['AGENTS.md', '.claude/AGENTS.md'] as const
@@ -1,0 +1,11 @@
1+/**
2+ * The engine's own instruction files, as it reads them in each directory of
3+ * its walk and under a read file.
4+ *
5+ * A directory holding one has project instructions the engine loads itself.
6+ */
7+export const CLAUDE_NAMES = [
8+ 'CLAUDE.md',
9+ '.claude/CLAUDE.md',
10+ 'CLAUDE.local.md',
11+] as const
@@ -1,0 +1,5 @@
1+export * from './agents-names.js'
2+export * from './claude-names.js'
3+export * from './main-loop.js'
4+
5+export * as default from '.'
@@ -1,0 +1,5 @@
1+/**
2+ * The key the main conversation's delivered files are kept under; a
3+ * subagent's are kept under its `agentId`.
4+ */
5+export const MAIN_LOOP = 'main'
@@ -1,0 +1,304 @@
1+import type {
2+ EngineInterface,
3+ FsAncestor,
4+ InstructionFile,
5+ On,
6+ PluginOptions,
7+} from 'claude-code'
8+
9+import Files from './files'
10+import Frames from './frames'
11+import Modes from './modes'
12+import Names from './names'
13+import Switches from './switches'
14+import Telemetry from './telemetry'
15+
16+/**
17+ * No files: what a walk not taken, or one that failed, found.
18+ */
19+const NONE: readonly FsAncestor[] = []
20+
21+/**
22+ * Registers the plugin's hooks for the mode `instructionFiles` names
23+ * (`claude-md-or-agents-md` when unset; the manifest lists the four, nothing
24+ * else arrives).
25+ *
26+ * `claude-md`: nothing beyond the usage row, the engine's CLAUDE.md walk
27+ * standing alone. `managed-only`: the project's and the person's instruction
28+ * files dropped, the organization's kept. `claude-md-or-agents-md` (a project
29+ * with none of its own) and `claude-md-and-agents-md`: AGENTS.md files joined
30+ * to the engine's instruction files, nested ones on a Read except in a run
31+ * where the engine attaches nothing to a turn (--bare, which sets
32+ * CLAUDE_CODE_SIMPLE, or CLAUDE_CODE_DISABLE_ATTACHMENTS). Every mode sends
33+ * its usage rows through `$.telemetry` where that noun is seated and drops
34+ * them where it is not.
35+ *
36+ * @param on the engine's registrar
37+ * @param options the plugin's options; `instructionFiles` is `claude-md`,
38+ * `claude-md-or-agents-md`, `claude-md-and-agents-md` or `managed-only`
39+ */
40+export function register(on: On, options: PluginOptions): void {
41+ const named = Modes.modeOf(options.instructionFiles)
42+ // COMPAT_BREAK(agents-md-project-instructions): drop the projectInstructions
43+ // mapping once stored settings have migrated. The host fills in the default,
44+ // so an instructionFiles set to it cannot be told from one left unset.
45+ const legacy = Modes.legacyModeOf(options.projectInstructions)
46+ const isLegacyRead = legacy !== undefined && named === Modes.DEFAULT_MODE
47+ const mode = isLegacyRead ? legacy : named
48+ let isRenameTold = legacy === undefined
49+
50+ on('session.start', ($, e, next) => {
51+ Telemetry.quietly(() =>
52+ $.telemetry.log(Telemetry.modeRowOf(mode, e.isInteractive)),
53+ )
54+
55+ if (!isRenameTold) {
56+ isRenameTold = true
57+ $.ui.log(
58+ isLegacyRead
59+ ? 'option projectInstructions in settings is honoured for now, ' +
60+ `read as instructionFiles ${mode}; set instructionFiles to ` +
61+ `${mode} and remove projectInstructions`
62+ : `option projectInstructions in settings is not read: ` +
63+ `instructionFiles ${mode} is set; remove projectInstructions`,
64+ )
65+ }
66+
67+ return next(e)
68+ })
69+
70+ if (mode === 'claude-md') {
71+ return
72+ }
73+
74+ if (mode === 'managed-only') {
75+ on(
76+ 'prompt.context',
77+ { instructionFiles: { kind: Files.DROPPED_KINDS } },
78+ ($, e, next) =>
79+ next({
80+ ...e,
81+ instructionFiles: e.instructionFiles?.filter(
82+ Files.isKeptWithoutInstructions,
83+ ),
84+ }),
85+ )
86+
87+ return
88+ }
89+
90+ const isFallback = mode === 'claude-md-or-agents-md'
91+ const given = new Map<string, Set<string>>()
92+ let inContext: readonly InstructionFile[] = []
93+ let home: string | undefined
94+ let isClaudeProject: boolean | undefined
95+ let rootSeen: string | undefined
96+ let rootLogged: string | undefined
97+ let isCounted = false
98+
99+ on('prompt.context', async ($, e, next) => {
100+ const handed = e.instructionFiles
101+
102+ if (handed === undefined) {
103+ return next(e).finally(() => {
104+ given.clear()
105+ })
106+ }
107+
108+ const root = isFallback ? await $.session.root() : undefined
109+ rootSeen = root ?? rootSeen
110+ let isWalkFailed = false
111+ // The walk, not only what was handed: the engine can withhold a project
112+ // CLAUDE.md it loaded, and the project still has one.
113+ isClaudeProject =
114+ root !== undefined &&
115+ (handed.some(file => Files.isClaudeFileOnWalk(file, root)) ||
116+ (await $.fs.ancestors({ names: Names.CLAUDE_NAMES }).then(
117+ files => files.length > 0,
118+ () => {
119+ isWalkFailed = true
120+
121+ return true
122+ },
123+ )))
124+ const found = isClaudeProject
125+ ? NONE
126+ : await $.fs.ancestors({ names: Names.AGENTS_NAMES }).catch(() => {
127+ isWalkFailed = true
128+
129+ return NONE
130+ })
131+ const added = Files.unseenFiles(Files.filesOf(found), handed)
132+
133+ if (!isCounted) {
134+ isCounted = true
135+ const counts = Telemetry.loadCountsOf(
136+ added,
137+ isClaudeProject && !isWalkFailed,
138+ isWalkFailed,
139+ )
140+ Telemetry.quietly(() =>
141+ $.telemetry.log(Telemetry.loadRowOf(mode, counts)),
142+ )
143+ Telemetry.quietly(() => $.telemetry.mark(Telemetry.loadMarkOf(counts)))
144+ }
145+
146+ const isFirstLoad =
147+ root !== undefined && root !== rootLogged && added.length > 0
148+
149+ if (isFirstLoad) {
150+ rootLogged = root
151+ $.ui.log(
152+ 'no CLAUDE.md found; AGENTS.md loaded: ' +
153+ added
154+ .filter(file => file.parent === undefined)
155+ .map(file => file.path)
156+ .join(', '),
157+ )
158+ }
159+
160+ const instructionFiles = Files.withProjectFiles(handed, added)
161+
162+ return next({ ...e, instructionFiles }).finally(() => {
163+ given.clear()
164+ inContext = instructionFiles
165+ })
166+ })
167+
168+ on('agent.spawn', { fork: true }, async ($, e, next) => {
169+ const result = await next(e)
170+
171+ if (result.agentId !== undefined) {
172+ const parent = given.get(e.parentAgentId ?? Names.MAIN_LOOP)
173+ given.set(result.agentId, new Set(parent))
174+ }
175+
176+ return result
177+ })
178+
179+ on('tool.call', { tool: 'Read' }, async ($, e, next) => {
180+ const result = await next(e)
181+ const isSettledElsewhere =
182+ e.tool !== 'Read' || result.deny !== undefined || result.isError
183+
184+ if (isSettledElsewhere) {
185+ return result
186+ }
187+
188+ if (!(await attachesOnRead($))) {
189+ return result
190+ }
191+
192+ const [root, cwd] = await Promise.all([$.session.root(), $.session.cwd()])
193+ home ??= await homeOf($, cwd)
194+ const read = Frames.absoluteOf(e.file_path, cwd, home)
195+
196+ if (root !== rootSeen) {
197+ rootSeen = root
198+ isClaudeProject = undefined
199+ given.clear()
200+ }
201+
202+ isClaudeProject ??=
203+ isFallback &&
204+ (await $.fs.ancestors({ names: Names.CLAUDE_NAMES })).length > 0
205+ const isOutOfReach = isClaudeProject || !Frames.isBelow(read, root)
206+
207+ if (isOutOfReach) {
208+ return result
209+ }
210+
211+ const [stack, claude] = await Promise.all([
212+ $.fs.ancestors({ names: Names.AGENTS_NAMES, of: read, below: root }),
213+ $.fs.ancestors({ names: Names.CLAUDE_NAMES, of: read, below: root }),
214+ ]).catch((): [typeof NONE, typeof NONE] => [NONE, NONE])
215+ const loop = e.agentId ?? Names.MAIN_LOOP
216+ const sent = given.get(loop) ?? new Set<string>()
217+ given.set(loop, sent)
218+ const nested = (
219+ isFallback ? Frames.outsideClaudeDirs(stack, claude) : stack
220+ ).filter(file => Frames.isBelow(file.dir, root))
221+ const fresh = Files.unseenFiles(Files.filesOf(nested), [
222+ ...inContext,
223+ ...Files.filesOf(claude),
224+ ]).filter(file => !sent.has(file.path))
225+ const attached = fresh.filter(file => !Frames.isFileAt(file, read))
226+ const isWholeRead = e.offset === undefined && e.limit === undefined
227+
228+ for (const file of fresh) {
229+ const isSent =
230+ attached.includes(file) || (Frames.isFileAt(file, read) && isWholeRead)
231+
232+ if (isSent) {
233+ sent.add(file.path)
234+ }
235+ }
236+
237+ const hasAttached = attached.length > 0
238+
239+ if (hasAttached) {
240+ Telemetry.quietly(() =>
241+ $.telemetry.log(Telemetry.nestedRowOf(mode, attached.length)),
242+ )
243+ }
244+
245+ return hasAttached
246+ ? {
247+ ...result,
248+ context: [
249+ ...(result.context ?? []),
250+ ...attached.map(Frames.nestedFrame),
251+ ],
252+ }
253+ : result
254+ })
255+}
256+
257+/**
258+ * Whether a Read attaches nested AGENTS.md files in this run: not where the
259+ * engine attaches nothing to a turn, a nested CLAUDE.md included, which is a
260+ * --bare run (it sets CLAUDE_CODE_SIMPLE) or one with
261+ * CLAUDE_CODE_DISABLE_ATTACHMENTS on.
262+ *
263+ * Read on every Read, as the engine reads them on every turn: a settings
264+ * `env` block or a managed delivery can flip either mid-session. The files of
265+ * the walk itself need no such check: where the engine loads no instruction
266+ * files `$.fs.ancestors` finds none.
267+ *
268+ * @param $ the engine, as the `tool.call` hook holds it
269+ * @returns whether nested files ride a Read's result here
270+ */
271+async function attachesOnRead($: EngineInterface): Promise<boolean> {
272+ const [simple, attachmentsOff] = await Promise.all([
273+ $.env.get('CLAUDE_CODE_SIMPLE'),
274+ $.env.get('CLAUDE_CODE_DISABLE_ATTACHMENTS'),
275+ ])
276+
277+ return (
278+ !Switches.isSwitchedOn(simple) && !Switches.isSwitchedOn(attachmentsOff)
279+ )
280+}
281+
282+/**
283+ * The home directory a `~` in a Read's path stands for, read the way the
284+ * Read tool reads it.
285+ *
286+ * The profile directory on a Windows spelling of the working directory,
287+ * else `HOME`, each falling back to the other.
288+ *
289+ * @param $ the engine, as the `tool.call` hook holds it
290+ * @param cwd the session's working directory, whose spelling names the platform
291+ * @returns the home directory, or undefined when the environment names none
292+ */
293+async function homeOf(
294+ $: EngineInterface,
295+ cwd: string,
296+): Promise<string | undefined> {
297+ const [home, profile] = await Promise.all([
298+ $.env.get('HOME'),
299+ $.env.get('USERPROFILE'),
300+ ])
301+ const isWindowsSpelling = cwd.includes('\\')
302+
303+ return isWindowsSpelling ? (profile ?? home) : (home ?? profile)
304+}
@@ -1,0 +1,3 @@
1+export * from './is-switched-on.js'
2+
3+export * as default from '.'
@@ -1,0 +1,14 @@
1+/**
2+ * The spellings an environment switch is on with, as the engine reads its own.
3+ */
4+const ON_SPELLINGS: ReadonlySet<string> = new Set(['1', 'true', 'yes', 'on'])
5+
6+/**
7+ * Whether an environment variable's value turns a switch on, read the way the
8+ * engine reads its own switches: `1`, `true`, `yes` or `on`, any case, trimmed.
9+ *
10+ * @param value the variable's value, or undefined when it is unset
11+ * @returns whether the switch is on
12+ */
13+export const isSwitchedOn = (value: string | undefined): boolean =>
14+ value !== undefined && ON_SPELLINGS.has(value.trim().toLowerCase())
@@ -1,0 +1,5 @@
1+/**
2+ * The plugin's name on the product-wide feature surface: its marks are
3+ * `$.telemetry.mark` rows carrying this `feature_name`.
4+ */
5+export const FEATURE_NAME = 'agents_md'
@@ -1,0 +1,15 @@
1+export * from './feature-name.js'
2+export * from './load-counts-of.js'
3+export * from './load-event.js'
4+export * from './load-mark-of.js'
5+export * from './load-row-of.js'
6+export * from './mode-choice-of.js'
7+export * from './mode-event.js'
8+export * from './mode-row-of.js'
9+export * from './nested-event.js'
10+export * from './nested-row-of.js'
11+export * from './quietly.js'
12+export * from './types'
13+export * from './walk-failed-reason.js'
14+
15+export * as default from '.'
@@ -1,0 +1,35 @@
1+import type { InstructionFile } from 'claude-code'
2+
3+import type { LoadCounts } from './types'
4+
5+/**
6+ * The first context's counts from the files it added: an entry with a parent
7+ * is an `@` import, one without is an AGENTS.md file.
8+ *
9+ * @param added the AGENTS.md instruction files handed to the engine, imports
10+ * among them
11+ * @param isYielded whether `claude-md-or-agents-md` stood down for a CLAUDE.md
12+ * @param isWalkFailed whether the walk threw
13+ * @returns the counts the load row and mark are built from
14+ */
15+export function loadCountsOf(
16+ added: readonly InstructionFile[],
17+ isYielded: boolean,
18+ isWalkFailed: boolean,
19+): LoadCounts {
20+ const importCount = added.reduce(
21+ (sum, file) => sum + (file.parent === undefined ? 0 : 1),
22+ 0,
23+ )
24+
25+ return {
26+ fileCount: added.length - importCount,
27+ importCount,
28+ totalContentLength: added.reduce(
29+ (sum, file) => sum + file.content.length,
30+ 0,
31+ ),
32+ isYielded,
33+ isWalkFailed,
34+ }
35+}
@@ -1,0 +1,7 @@
1+/**
2+ * The row the first context of a load sends under `claude-md-or-agents-md` and
3+ * `claude-md-and-agents-md`: what was handed on, in counts.
4+ *
5+ * The engine's own initial-load row counts its CLAUDE.md files the same way.
6+ */
7+export const LOAD_EVENT = 'agents_md_load'
@@ -1,0 +1,15 @@
1+import { FEATURE_NAME } from './feature-name.js'
2+import type { LoadCounts, TelemetryMark } from './types'
3+import { WALK_FAILED_REASON } from './walk-failed-reason.js'
4+
5+/**
6+ * The first context's feature mark: `ok`, or `sad` when the walk failed and
7+ * the context went on without AGENTS.md.
8+ *
9+ * @param counts what the first context handed on
10+ * @returns the mark to send
11+ */
12+export const loadMarkOf = (counts: LoadCounts): TelemetryMark =>
13+ counts.isWalkFailed
14+ ? { feature: FEATURE_NAME, kind: 'sad', reason: WALK_FAILED_REASON }
15+ : { feature: FEATURE_NAME, kind: 'ok' }
@@ -1,0 +1,27 @@
1+import type Modes from '../modes'
2+import { LOAD_EVENT } from './load-event.js'
3+import { modeChoiceOf } from './mode-choice-of.js'
4+import type { LoadCounts, TelemetryRow } from './types'
5+
6+/**
7+ * The first-context row: the mode and what was handed to the engine, in
8+ * counts only; no path and no text.
9+ *
10+ * @param mode the configured mode
11+ * @param counts what the first context handed on
12+ * @returns the row to send
13+ */
14+export const loadRowOf = (
15+ mode: Modes.Mode,
16+ counts: LoadCounts,
17+): TelemetryRow => ({
18+ event: LOAD_EVENT,
19+ props: {
20+ mode: modeChoiceOf(mode),
21+ file_count: counts.fileCount,
22+ import_count: counts.importCount,
23+ total_content_length: counts.totalContentLength,
24+ yielded: counts.isYielded,
25+ walk_failed: counts.isWalkFailed,
26+ },
27+})
@@ -1,0 +1,13 @@
1+import Modes from '../modes'
2+import type { TelemetryProp } from './types'
3+
4+/**
5+ * The mode as a row carries it: the value with the four it is chosen from.
6+ *
7+ * @param mode the configured mode
8+ * @returns the `mode` property
9+ */
10+export const modeChoiceOf = (mode: Modes.Mode): TelemetryProp => ({
11+ value: mode,
12+ of: Modes.MODES,
13+})
@@ -1,0 +1,5 @@
1+/**
2+ * The row a fresh load sends at `session.start`, whatever the mode: which
3+ * mode is configured.
4+ */
5+export const MODE_EVENT = 'agents_md_mode'
@@ -1,0 +1,20 @@
1+import type Modes from '../modes'
2+import { modeChoiceOf } from './mode-choice-of.js'
3+import { MODE_EVENT } from './mode-event.js'
4+import type { TelemetryRow } from './types'
5+
6+/**
7+ * The `session.start` row: the configured mode and whether a person is at
8+ * the terminal.
9+ *
10+ * @param mode the configured mode
11+ * @param isInteractive whether a person is at the terminal
12+ * @returns the row to send
13+ */
14+export const modeRowOf = (
15+ mode: Modes.Mode,
16+ isInteractive: boolean,
17+): TelemetryRow => ({
18+ event: MODE_EVENT,
19+ props: { mode: modeChoiceOf(mode), is_interactive: isInteractive },
20+})
@@ -1,0 +1,5 @@
1+/**
2+ * The row a Read sends when it attached nested AGENTS.md files; a Read that
3+ * attached none sends nothing.
4+ */
5+export const NESTED_EVENT = 'agents_md_nested'
@@ -1,0 +1,20 @@
1+import type Modes from '../modes'
2+import { modeChoiceOf } from './mode-choice-of.js'
3+import { NESTED_EVENT } from './nested-event.js'
4+import type { TelemetryRow } from './types'
5+
6+/**
7+ * The row of a Read that attached nested AGENTS.md files: the mode and how
8+ * many it attached.
9+ *
10+ * @param mode the configured mode
11+ * @param fileCount the files attached to this Read's result
12+ * @returns the row to send
13+ */
14+export const nestedRowOf = (
15+ mode: Modes.Mode,
16+ fileCount: number,
17+): TelemetryRow => ({
18+ event: NESTED_EVENT,
19+ props: { mode: modeChoiceOf(mode), file_count: fileCount },
20+})
@@ -1,0 +1,13 @@
1+/**
2+ * Runs one `$.telemetry` call and drops whatever it throws or rejects with.
3+ *
4+ * A build with no telemetry noun, a refused row or an ingest that will not
5+ * take it never disturbs the instructions.
6+ *
7+ * @param send the call
8+ */
9+export const quietly = (send: () => Promise<void>): void => {
10+ void Promise.resolve()
11+ .then(send)
12+ .catch(() => undefined)
13+}
@@ -1,0 +1,6 @@
1+export * from './load-counts'
2+export * from './telemetry-mark'
3+export * from './telemetry-prop'
4+export * from './telemetry-row'
5+
6+export * as default from '.'
@@ -1,0 +1,3 @@
1+export type * from './load-counts.js'
2+
3+export * as default from '.'
@@ -1,0 +1,14 @@
1+/**
2+ * What the first context of a load handed the engine, as counts.
3+ *
4+ * The AGENTS.md files added, the files they `@`-imported, their characters
5+ * in all, whether `claude-md-or-agents-md` stood down for a CLAUDE.md of the
6+ * project's own, and whether the walk threw so that nothing was found.
7+ */
8+export type LoadCounts = {
9+ fileCount: number
10+ importCount: number
11+ totalContentLength: number
12+ isYielded: boolean
13+ isWalkFailed: boolean
14+}
@@ -1,0 +1,3 @@
1+export type * from './telemetry-mark.js'
2+
3+export * as default from '.'
@@ -1,0 +1,7 @@
1+import type { EngineInterface } from 'claude-code'
2+
3+/**
4+ * What `$.telemetry.mark` takes, as the telemetry mod's contract declares
5+ * it: the feature, how it went, and why when not ok.
6+ */
7+export type TelemetryMark = Parameters<EngineInterface['telemetry']['mark']>[0]
@@ -1,0 +1,3 @@
1+export type * from './telemetry-prop.js'
2+
3+export * as default from '.'
@@ -1,0 +1,9 @@
1+import type { TelemetryRow } from '../telemetry-row'
2+
3+/**
4+ * One property value a `$.telemetry` row may carry.
5+ *
6+ * As the telemetry mod's contract declares it: a finite number, a boolean,
7+ * or a string chosen from the list declared beside it.
8+ */
9+export type TelemetryProp = NonNullable<TelemetryRow['props']>[string]
@@ -1,0 +1,3 @@
1+export type * from './telemetry-row.js'
2+
3+export * as default from '.'
@@ -1,0 +1,7 @@
1+import type { EngineInterface } from 'claude-code'
2+
3+/**
4+ * What `$.telemetry.log` takes, as the telemetry mod's contract declares
5+ * it: the event's name and its properties by snake_case key.
6+ */
7+export type TelemetryRow = Parameters<EngineInterface['telemetry']['log']>[0]
@@ -1,0 +1,5 @@
1+/**
2+ * The `sad` mark's reason when the AGENTS.md walk threw and the load went on
3+ * with nothing found.
4+ */
5+export const WALK_FAILED_REASON = 'walk_failed'
@@ -1,0 +1,20 @@
1+import type { FsAncestor } from 'claude-code'
2+
3+/**
4+ * One instruction file a walk found.
5+ *
6+ * @param dir the directory the file stands in
7+ * @param name the file's name as the walk asked for it
8+ * @param content the file's text
9+ * @returns the found file
10+ */
11+export const ancestorOf = (
12+ dir: string,
13+ name: string,
14+ content: string,
15+): FsAncestor => ({
16+ dir,
17+ name,
18+ content,
19+ parts: [{ path: `${dir}/${name}`, content }],
20+})
@@ -1,0 +1,10 @@
1+export * from './ancestor-of.js'
2+export * from './project-of.js'
3+export * from './recording.js'
4+export * from './rows-of.js'
5+export * from './session.js'
6+export * from './started-of.js'
7+export * from './types'
8+export * from './unreadable-project-of.js'
9+
10+export * as default from '.'
@@ -1,0 +1,31 @@
1+import type { FsAncestor, On } from 'claude-code'
2+
3+import { startedOf } from './started-of.js'
4+import type { Started } from './types'
5+
6+/**
7+ * A project whose walks up from the working directory find the given files,
8+ * one list per set of names, its session starting untouched (startedOf);
9+ * each walk's first name is kept in `walks`.
10+ *
11+ * @param on the test's `on`
12+ * @param agents what a walk for AGENTS.md and .claude/AGENTS.md finds
13+ * @param claude what a walk for CLAUDE.md, .claude/CLAUDE.md and
14+ * CLAUDE.local.md finds
15+ * @returns the toasts, lines and walks the plugins raised, in order
16+ */
17+export function projectOf(
18+ on: On,
19+ agents: readonly FsAncestor[],
20+ claude: readonly FsAncestor[],
21+): Started {
22+ const started = startedOf(on)
23+
24+ on('fs.ancestors', ($, e) => {
25+ started.walks.push(e.names[0] ?? '')
26+
27+ return { value: e.names.includes('AGENTS.md') ? agents : claude }
28+ })
29+
30+ return started
31+}
@@ -1,0 +1,29 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * A plugin standing in for the telemetry built-in: it adds the `telemetry`
5+ * noun at `engine.create` over the nouns beneath.
6+ *
7+ * Its `log` and `mark` write the entry they are handed as one `$.ui.log`
8+ * line, `<op> <json>`, which startedOf keeps and rowsOf reads back.
9+ */
10+export const RECORDING: Plugin = {
11+ name: 'recording',
12+ tier: 'builtin',
13+ register(on) {
14+ on('engine.create', async ($, e, next) => {
15+ const beneath = await next(e)
16+ const built = { ...beneath }
17+ built.telemetry = {
18+ log: async entry => {
19+ await beneath.ui.log(`log ${JSON.stringify(entry)}`)
20+ },
21+ mark: async entry => {
22+ await beneath.ui.log(`mark ${JSON.stringify(entry)}`)
23+ },
24+ }
25+
26+ return built
27+ })
28+ },
29+}
@@ -1,0 +1,17 @@
1+import type { Row, Started } from './types'
2+
3+/**
4+ * What the plugin handed the telemetry noun so far, read off the transcript
5+ * lines RECORDING wrote, in the order handed; other lines are passed over.
6+ *
7+ * @param started the session's start, whose lines hold the rows
8+ * @returns the `[op, entry]` pairs
9+ */
10+export const rowsOf = (started: Started): Row[] =>
11+ started.lines
12+ .filter(line => line.startsWith('log ') || line.startsWith('mark '))
13+ .map((line): Row => {
14+ const at = line.indexOf(' ')
15+
16+ return [line.slice(0, at), JSON.parse(line.slice(at + 1))]
17+ })
@@ -1,0 +1,10 @@
1+import type { SessionStartInput } from 'claude-code'
2+
3+/**
4+ * An interactive terminal session two directories under /repo.
5+ */
6+export const SESSION: SessionStartInput = {
7+ surface: 'terminal',
8+ isInteractive: true,
9+ cwd: '/repo/a/b',
10+}
@@ -1,0 +1,36 @@
1+import type { On } from 'claude-code'
2+import { mock } from 'claude-code/testing'
3+
4+import { SESSION } from './session.js'
5+import type { Started, ToastAsked } from './types'
6+
7+/**
8+ * A session that starts untouched beneath the plugins, rooted at SESSION's
9+ * working directory, every toast and transcript line they raise kept for the
10+ * test to read, on a mock clock the test settles before it reads them.
11+ *
12+ * @param on the test's `on`
13+ * @returns the toasts, lines and walks raised, in order, and the clock
14+ */
15+export function startedOf(on: On): Started {
16+ const toasts: ToastAsked[] = []
17+ const lines: string[] = []
18+
19+ on('session.start', ($, e) => ({ cwd: e.cwd }))
20+
21+ on('session.root', () => ({ value: SESSION.cwd }))
22+
23+ on('ui.toast', ($, e) => {
24+ toasts.push(e)
25+
26+ return { value: undefined }
27+ })
28+
29+ on('ui.log', ($, e) => {
30+ lines.push(e.text)
31+
32+ return { value: undefined }
33+ })
34+
35+ return { toasts, lines, walks: [], clock: mock.clock(on) }
36+}
@@ -1,0 +1,5 @@
1+export * from './row'
2+export * from './started'
3+export * from './toast-asked'
4+
5+export * as default from '.'
@@ -1,0 +1,3 @@
1+export type * from './row.js'
2+
3+export * as default from '.'
@@ -1,0 +1,5 @@
1+/**
2+ * One entry the plugin handed the telemetry noun, as RECORDING wrote it:
3+ * which call (`log` or `mark`) and the entry.
4+ */
5+export type Row = [op: string, entry: unknown]
@@ -1,0 +1,3 @@
1+export type * from './started.js'
2+
3+export * as default from '.'
@@ -1,0 +1,15 @@
1+import type { MockClock } from 'claude-code/testing'
2+
3+import type { ToastAsked } from '../toast-asked'
4+
5+/**
6+ * What the plugins raised while a session ran: each toast as asked for, each
7+ * transcript line, the first name of each walk they asked for; `clock`
8+ * settles what they floated.
9+ */
10+export type Started = {
11+ toasts: ToastAsked[]
12+ lines: string[]
13+ walks: string[]
14+ clock: MockClock
15+}
@@ -1,0 +1,3 @@
1+export type * from './toast-asked.js'
2+
3+export * as default from '.'
@@ -1,0 +1,8 @@
1+/**
2+ * One `$.ui.toast` call as the plugin made it: the text and, when it named
3+ * one, how long the bar keeps it.
4+ */
5+export type ToastAsked = {
6+ text: string
7+ timeoutMs?: number
8+}
@@ -1,0 +1,20 @@
1+import type { On } from 'claude-code'
2+
3+import { startedOf } from './started-of.js'
4+import type { Started } from './types'
5+
6+/**
7+ * A project whose walk fails, its session starting untouched (startedOf).
8+ *
9+ * @param on the test's `on`
10+ * @returns the toasts and lines the plugins raised, in order
11+ */
12+export function unreadableProjectOf(on: On): Started {
13+ const started = startedOf(on)
14+
15+ on('fs.ancestors', () => {
16+ throw new Error('no walk today')
17+ })
18+
19+ return started
20+}
@@ -1,0 +1,137 @@
1+import { describe, expect, test, tier } from 'claude-code/testing'
2+
3+import Hooks from '../hooks'
4+import Fixtures from './fixtures'
5+
6+tier('builtin')
7+
8+/**
9+ * The blocks the engine hands `prompt.context`.
10+ */
11+const BLOCKS = [
12+ { name: 'claudeMd', text: 'x' },
13+ { name: 'currentDate', text: 'today' },
14+]
15+
16+describe('register', () => {
17+ test('by default a project with AGENTS.md alone gets it', async ($, on) => {
18+ const started = Fixtures.projectOf(
19+ on,
20+ [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
21+ [],
22+ )
23+
24+ on('prompt.context', ($, e) => ({
25+ blocks: e.blocks,
26+ instructionFiles: e.instructionFiles,
27+ }))
28+
29+ const context = await $.prompt.context({
30+ blocks: BLOCKS,
31+ instructionFiles: [],
32+ })
33+
34+ expect(context.instructionFiles).toEqual([
35+ { path: '/repo/AGENTS.md', kind: 'project', content: '# top\n' },
36+ ])
37+ expect(context.blocks?.[0]?.text).toContain(
38+ 'Contents of /repo/AGENTS.md (project instructions, checked into the ' +
39+ 'codebase):\n\n# top',
40+ )
41+
42+ await started.clock.settle()
43+
44+ expect(started.lines).toEqual([
45+ 'no CLAUDE.md found; AGENTS.md loaded: /repo/AGENTS.md',
46+ ])
47+ })
48+
49+ test('by default a project with a CLAUDE.md is left to the engine', async ($, on) => {
50+ const started = Fixtures.projectOf(
51+ on,
52+ [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
53+ [Fixtures.ancestorOf('/repo/a/b', 'CLAUDE.md', '# mine\n')],
54+ )
55+ const handed = [
56+ { path: '/repo/a/b/CLAUDE.md', kind: 'project' as const, content: '#' },
57+ ]
58+
59+ on('prompt.context', ($, e) => ({
60+ blocks: e.blocks,
61+ instructionFiles: e.instructionFiles,
62+ }))
63+
64+ expect(
65+ await $.prompt.context({ blocks: BLOCKS, instructionFiles: handed }),
66+ ).toEqual({ blocks: BLOCKS, instructionFiles: handed })
67+
68+ await started.clock.settle()
69+
70+ expect(started.walks).toEqual([])
71+ expect(started.lines).toEqual([])
72+ })
73+
74+ test('a failed walk leaves the context as handed', async ($, on) => {
75+ Fixtures.unreadableProjectOf(on)
76+
77+ on('prompt.context', ($, e) => ({
78+ blocks: e.blocks,
79+ instructionFiles: e.instructionFiles,
80+ }))
81+
82+ expect(
83+ await $.prompt.context({ blocks: BLOCKS, instructionFiles: [] }),
84+ ).toEqual({ blocks: BLOCKS, instructionFiles: [] })
85+ })
86+
87+ test(
88+ 'the start sends the mode row alone: no walk, no toast',
89+ { plugins: [Fixtures.RECORDING] },
90+ async ($, on) => {
91+ const started = Fixtures.projectOf(
92+ on,
93+ [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
94+ [],
95+ )
96+
97+ expect(await $.session.start(Fixtures.SESSION)).toEqual({
98+ cwd: '/repo/a/b',
99+ })
100+
101+ await started.clock.settle()
102+
103+ expect(Fixtures.rowsOf(started)).toEqual([
104+ [
105+ 'log',
106+ {
107+ event: Hooks.MODE_EVENT,
108+ props: {
109+ mode: { value: 'claude-md-or-agents-md', of: [...Hooks.MODES] },
110+ is_interactive: true,
111+ },
112+ },
113+ ],
114+ ])
115+ expect(Hooks.MODE_EVENT).toBe('agents_md_mode')
116+ expect(started.walks).toEqual([])
117+ expect(started.toasts).toEqual([])
118+ },
119+ )
120+
121+ test('with no telemetry noun the start goes on untouched', async ($, on) => {
122+ const started = Fixtures.projectOf(
123+ on,
124+ [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
125+ [],
126+ )
127+
128+ expect(
129+ await $.session.start({ ...Fixtures.SESSION, isInteractive: false }),
130+ ).toEqual({ cwd: '/repo/a/b' })
131+
132+ await started.clock.settle()
133+
134+ expect(Fixtures.rowsOf(started)).toEqual([])
135+ expect(started.toasts).toEqual([])
136+ })
137+})
@@ -13,14 +13,18 @@
1313 through Buttons that declare the engine's own actions. The pane refreshes
1414 as Claude edits and runs shell commands, and while it is open it polls
1515 the repository's HEAD so a commit or checkout made elsewhere shows too.
16-The first successful edit of a session the diff has a file to list for
17-opens the pane by itself where the layout docks it beside the transcript
18-(the fullscreen layout, which each drawing's `viewport` says) and the
19-terminal is wide enough (144 columns when the person never chose, 110 when
20-they kept it open before; a person who closed it is left alone); where the
21-surface does not say, nothing opens by itself. An edit to a file outside
22-the repository, or one after which the diff lists nothing or cannot be
23-read, opens nothing and leaves the opening to a later edit.
16+The main loop's first successful edit of a session the diff has a file to
17+list for opens the pane by itself, as the built-in panel opens on its first
18+checkpoint: where the layout docks it beside the transcript (the fullscreen
19+layout, which each drawing's `viewport` says), the terminal is wide enough
20+(144 columns when the person never chose, 110 when they kept it open
21+before; a person who closed it is left alone) and file checkpointing is on;
22+a subagent's edit opens nothing, and where the surface does not say,
23+nothing opens by itself. An edit to a file outside the repository, or one
24+after which the diff lists nothing or cannot be read, opens nothing and
25+leaves the opening to a later edit. An open the engine leaves waiting
26+undrawn is withdrawn, so no later resize seats it, and the next edit asks
27+again.
2428
2529 Under the fullscreen layout a terminal under 110 columns gets the
2630 built-in's line asking for a wider one and nothing opens. Without that
@@ -53,9 +57,9 @@
5357 forgets it); and the working tree is read only by a fetch for a pane that
5458 is open, after an edit that landed or a shell command that ran, or by the
5559 fetch each edit inside the repository, with room for a pane, makes until
56-one lists a file to open on. The one read the built-in has no counterpart
57-for is a `git status` at a pane's first fetch, which stands in for the
58-change time the built-in dates a moved file by.
60+one lists a file and the pane opens on it. The one read the built-in has
61+no counterpart for is a `git status` at a pane's first fetch, which stands
62+in for the change time the built-in dates a moved file by.
5963
6064 `hooks/register.ts` is the module; everything under `hooks/` is its parts.
6165
@@ -71,16 +75,17 @@
7175 | `ui.scroll` of the pane | Docked, moves the hunks under the pinned header and list (three rows a wheel tick, a page a page key), or the list when the wheel is over it, and keeps the engine's window still. |
7276 | `ui.focus` in the pane | In the dialog's list, selects the file the ring lands on, re-centres the five rows on it, and lands the ring where that row now sits. |
7377 | `command.run` of `clear`, `resume` | Closes the pane and forgets the session's state, the pinned repository with it. |
74-| `tool.call` of `Edit`, `Write`, `NotebookEdit` | After an edit that landed (not refused, not failed), refreshes an open pane; the session's first such edit inside the repository whose fetch lists a file opens it on that fetch, pinning the repository then if the terminal has the room. |
78+| `tool.call` of `Edit`, `Write`, `NotebookEdit` | After an edit that landed (not refused, not failed), refreshes an open pane; the main loop's first such edit inside the repository whose fetch lists a file opens it on that fetch, pinning the repository then if the terminal has the room and checkpointing is on. |
7579 | `tool.call` of `Bash`, `PowerShell` | After a command that was not refused, failed and interrupted ones too, refreshes an open pane. |
7680 | `prompt.submit` | Adds the armed file's hunks to the prompt's context and disarms. |
7781
7882 ## What it calls on `$`
7983
80-`clock.after`, `clock.every`, `clock.now`, `command.register`, `fs.list`,
81-`fs.read`, `fs.stat`, `process.run` (git, read-only), `session.messages`,
82-`store.get`, `store.set`, `telemetry.log`, `telemetry.mark`, `ui.close`,
83-`ui.invalidate`, `ui.log`, `ui.open`, `ui.resolve`, `ui.status`.
84+`clock.after`, `clock.every`, `clock.now`, `command.register`, `env.get`
85+(`CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING`), `fs.list`, `fs.read`, `fs.stat`,
86+`process.run` (git, read-only), `session.id`, `session.messages`,
87+`settings.read`, `store.get`, `store.set`, `telemetry.log`, `telemetry.mark`,
88+`ui.close`, `ui.invalidate`, `ui.log`, `ui.open`, `ui.resolve`, `ui.status`.
8489
8590 `$.telemetry` is the telemetry plugin's noun; where it is absent the rows
8691 are dropped and nothing else changes.
@@ -65,6 +65,12 @@
6565 storeSet: (key: string, value: unknown) => Promise<void>
6666
6767 /**
68+ * Whether the session checkpoints edits (`$.settings.read`, `$.env.get`):
69+ * the built-in panel opens on an edit only while it does.
70+ */
71+ isCheckpointing: () => Promise<boolean>
72+
73+ /**
6874 * `$.session.messages`.
6975 */
7076 messages: () => Promise<SessionMessage[]>
@@ -87,7 +93,7 @@
8793 /**
8894 * `$.ui.open`.
8995 */
90- openPane: (pane: PaneOpenArgs) => Promise<void>
96+ openPane: (pane: PaneOpenArgs) => Promise<unknown>
9197
9298 /**
9399 * `$.ui.close`.
@@ -8,6 +8,7 @@
88 export * from './entry-kinds-of'
99 export * from './git'
1010 export * from './host'
11+export * from './is-checkpointing'
1112 export * from './is-on-pane-surface'
1213 export * from './is-outside-working-tree'
1314 export * from './is-record'
@@ -1,0 +1,3 @@
1+export * from './is-checkpointing.js'
2+
3+export * as default from '.'
@@ -1,0 +1,20 @@
1+import type { Settings } from 'claude-code'
2+
3+/**
4+ * Whether the session checkpoints Claude's edits, read as the built-in
5+ * reads it: the setting on unless set false, the variable unset or falsy.
6+ *
7+ * The built-in panel opens on an edit only through a checkpoint, so with
8+ * checkpointing off its first-edit open never happens; `/diff` still opens.
9+ *
10+ * @param settings the merged settings (`$.settings.read()`)
11+ * @param disabling `CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING` as `$.env.get`
12+ * answers it
13+ * @returns false when either turns checkpointing off
14+ */
15+export const isCheckpointing = (
16+ settings: Settings,
17+ disabling: string | undefined,
18+): boolean =>
19+ settings.fileCheckpointingEnabled !== false &&
20+ !['1', 'true', 'yes', 'on'].includes((disabling ?? '').trim().toLowerCase())
@@ -1,10 +1,10 @@
11 import type {
2+ Args,
23 On,
34 PaneOpenArgs,
45 ResultOf,
56 SessionMessage,
67 Timer,
7- ToolCallInput,
88 } from 'claude-code'
99
1010 import Ask from './ask'
@@ -14,8 +14,10 @@
1414 import { entryKindsOf } from './entry-kinds-of'
1515 import type Git from './git'
1616 import type { Host } from './host'
17+import { isCheckpointing } from './is-checkpointing'
1718 import { isOnPaneSurface } from './is-on-pane-surface'
1819 import { isOutsideWorkingTree } from './is-outside-working-tree'
20+import { isRecord } from './is-record'
1921 import Limits from './limits'
2022 import { mapLimited } from './map-limited'
2123 import { messageOf } from './message-of'
@@ -32,9 +34,9 @@
3234 * pane's drawing and refresh, its opening on Claude's first edit, the ask.
3335 *
3436 * Git runs when the built-in's would: `session.start` binds the host and
35- * registers `/diff`; `/diff` or the first edit with room pins the backend
36- * where the session started, until `/clear`; an open pane fetches, and so
37- * does an edit with room, inside the tree, until one lists a file to open on.
37+ * registers `/diff`; `/diff` or the main loop's first checkpointed edit with
38+ * room pins the backend, until `/clear`; a placed, open pane fetches, and so
39+ * does such an edit inside the tree, until one lists a file to open on.
3840 *
3941 * @param on the engine's registrar
4042 */
@@ -414,7 +416,7 @@
414416 engine: Host,
415417 trigger: (typeof Record.SHOWN_TRIGGERS)[number],
416418 read: PaneState.Fetched | null = null,
417- ): Promise<void> {
419+ ): Promise<boolean> {
418420 const isDialog = model.isFullscreen === false
419421
420422 opens += 1
@@ -428,14 +430,24 @@
428430
429431 dialogRows = isDialog ? Views.dialogRowsOf(model) : null
430432
431- const settling = read ? refresh(engine, read) : null
433+ if (read) {
434+ model = PaneState.afterFetch(model, read)
435+ }
432436
433- await engine.openPane(
437+ const opened = await engine.openPane(
434438 isDialog
435439 ? { ...dialogPane(), focus: true }
436440 : { id: Names.PANE_ID, title: Names.PANE_TITLE, holdToasts: true },
437441 )
438442
443+ const isWaiting = isRecord(opened) && opened.isPlaced === false
444+
445+ if (isWaiting) {
446+ await engine.closePane({ id: Names.PANE_ID }).catch(() => undefined)
447+
448+ return false
449+ }
450+
439451 isPaneOpen = true
440452
441453 const sessionId = await engine.sessionId().catch(() => null)
@@ -445,7 +457,9 @@
445457 Record.recorderOf(engine).shown(trigger, Record.widthBucketOf(columns))
446458 }
447459
448- void (settling ?? refresh(engine))
460+ void refresh(engine, read)
461+
462+ return true
449463 }
450464
451465 async function closePane(engine: Host): Promise<void> {
@@ -490,7 +504,14 @@
490504
491505 if (isListing) {
492506 hasAutoOpened = true
493- await openPane(engine, 'auto_open', read)
507+
508+ const isPlaced = await openPane(engine, 'auto_open', read)
509+
510+ if (!isPlaced) {
511+ hasAutoOpened = false
512+
513+ return
514+ }
494515 }
495516
496517 if (seen === landed || isOvertaken) {
@@ -527,6 +548,12 @@
527548 return
528549 }
529550
551+ const isCheckpointed = await engine.isCheckpointing().catch(() => true)
552+
553+ if (!isCheckpointed || isTaken()) {
554+ return
555+ }
556+
530557 await pinBackend(engine)
531558
532559 if (!backend || isTaken()) {
@@ -677,6 +704,11 @@
677704 readFile: path => $.fs.read(path),
678705 storeGet: key => $.store.get(key),
679706 storeSet: (key, value) => $.store.set(key, value),
707+ isCheckpointing: async () =>
708+ isCheckpointing(
709+ await $.settings.read(),
710+ await $.env.get('CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING'),
711+ ),
680712 messages: () => $.session.messages(),
681713 invalidate: () => $.ui.invalidate('ui.render'),
682714 status: text => $.ui.status(text),
@@ -774,8 +806,15 @@
774806 }
775807
776808 const isOpening = toggle === 'open'
777- await (isOpening ? openPane(host, 'manual') : closePane(host))
778809
810+ const isDone = isOpening
811+ ? await openPane(host, 'manual')
812+ : await closePane(host).then(() => true)
813+
814+ if (!isDone) {
815+ return { text: Names.RESIZE_TERMINAL_TEXT }
816+ }
817+
779818 if (!isFullscreen) {
780819 return isOpening ? {} : { text: Names.DIALOG_DISMISSED_TEXT }
781820 }
@@ -893,7 +932,7 @@
893932
894933 function afterTool(
895934 engine: Host,
896- e: ToolCallInput,
935+ e: Args<'tool.call'>,
897936 result: ResultOf['tool.call'] | undefined,
898937 ) {
899938 const isEdit = Tools.EDITING_TOOLS.some(name => name === e.tool)
@@ -916,7 +955,9 @@
916955 scheduleRefresh(engine)
917956 }
918957
919- if (hasEdited) {
958+ const isMainLoopEdit = hasEdited && e.agentId === undefined
959+
960+ if (isMainLoopEdit) {
920961 void openOnFirstEdit(engine, Tools.editedPathOf(e)).catch(() => undefined)
921962 }
922963 }
@@ -1,0 +1,28 @@
1+import type { Settings } from 'claude-code'
2+
3+/**
4+ * What the world beneath a repository session holds and answers besides
5+ * git: the store, the settings, the environment, whether a pane is seated.
6+ */
7+export type Beneath = {
8+ /**
9+ * What the plugin's store holds at the start; nothing when not given.
10+ */
11+ stored?: Readonly<Record<string, unknown>>
12+
13+ /**
14+ * What `$.settings.read` answers; empty when not given.
15+ */
16+ settings?: Settings
17+
18+ /**
19+ * The variables `$.env.get` answers from; none set when not given.
20+ */
21+ env?: Readonly<Record<string, string>>
22+
23+ /**
24+ * Whether an open made now is left waiting undrawn (LEFT_WAITING), as an
25+ * engine leaves an unasked open on a narrow terminal; placed when not given.
26+ */
27+ isLeftWaiting?: () => boolean
28+}
@@ -1,0 +1,4 @@
1+export type * from './beneath.js'
2+export * from './left-waiting.js'
3+
4+export * as default from '.'
@@ -1,0 +1,17 @@
1+import type { ResultOf } from 'claude-code'
2+
3+/**
4+ * What an engine answers an open it leaves waiting undrawn: the plugin
5+ * opened unasked on a terminal narrower than an unrequested pane is given.
6+ *
7+ * Typed through `never` so it compiles against declarations that predate the
8+ * answer, where `ui.open` resolves nothing.
9+ */
10+export const LEFT_WAITING: ResultOf['ui.open'] = {
11+ value: {
12+ isPlaced: false,
13+ reason:
14+ 'unasked below 144 columns (120 now): placed when the person opens ' +
15+ 'it, or when the terminal is widened to 144 columns',
16+ } as never,
17+}
@@ -1,6 +1,7 @@
11 import type { Args, On } from 'claude-code'
22 import { mock } from 'claude-code/testing'
33
4+import Beneath from './beneath'
45 import { gitIn } from './git-in.js'
56 import { HINT_DRAWN } from './hint-drawn.js'
67 import { keeping } from './keeping.js'
@@ -11,24 +12,25 @@
1112 * A session in a repository git answers for from a script (REPOSITORY, in
1213 * /work, when none is given), keeping what the plugin does there.
1314 *
14- * Kept: each git run, ring move, pane opened or closed, status line. The clock
15- * starts at 0 and the engine draws the hint. A test that rewrites the
16- * script between calls changes what git answers next.
15+ * Kept: each git run, ring move, pane opened, left waiting or closed, status
16+ * line. The clock starts at 0 and the engine draws the hint; the rest of the
17+ * world is the test's (Beneath). Rewriting the script changes git's answers.
1718 *
1819 * @param on the test's `on`
1920 * @param script git's output for each invocation whose line holds the key
20- * @param stored what the plugin's store holds at the start
21- * @returns the runs, the ring's moves, the panes opened and closed, the
22- * statuses, the clock
21+ * @param beneath the store, settings, environment, whether an open is seated
22+ * @returns the runs, the ring's moves, the panes opened, left waiting and
23+ * closed, the statuses, the clock
2324 */
2425 export function inRepository(
2526 on: On,
2627 script: Readonly<Record<string, string>> = REPOSITORY,
27- stored: Readonly<Record<string, unknown>> = {},
28+ beneath: Beneath.Beneath = {},
2829 ) {
2930 const runs: Args<'process.run'>[] = []
3031 const focused: Args<'ui.focus'>[] = []
3132 const statuses: (string | undefined)[] = []
33+ const waiting: Args<'ui.open'>[] = []
3234 const opened = keeping<Args<'ui.open'>>()
3335 const closed = keeping<Args<'ui.close'>>()
3436 const clock = startsSession(on)
@@ -51,17 +53,31 @@
5153 return { value: undefined }
5254 })
5355
54- on('ui.open', opened.hook)
56+ on('ui.open', (engine, e) => {
57+ const isWaiting = beneath.isLeftWaiting?.() === true
58+
59+ if (!isWaiting) {
60+ return opened.hook(engine, e)
61+ }
62+
63+ waiting.push(e)
64+
65+ return Beneath.LEFT_WAITING
66+ })
67+
5568 on('ui.close', closed.hook)
5669 on('ui.invalidate', () => ({ value: undefined }))
5770 on('ui.render', { component: 'PromptHint' }, () => HINT_DRAWN)
5871 on('session.messages', () => ({ value: [] }))
59- mock.store(on, stored)
72+ on('settings.read', () => ({ value: beneath.settings ?? {} }))
73+ mock.store(on, beneath.stored ?? {})
74+ mock.env(on, beneath.env ?? {})
6075
6176 return {
6277 runs,
6378 focused,
6479 opened: opened.kept,
80+ waiting,
6581 closed: closed.kept,
6682 statuses,
6783 clock,
@@ -45,7 +45,9 @@
4545 on('ui.invalidate', () => ({ value: undefined }))
4646 on('ui.render', { component: 'PromptHint' }, () => HINT_DRAWN)
4747 on('session.messages', () => ({ value: [] }))
48+ on('settings.read', () => ({ value: {} }))
4849 mock.store(on, {})
50+ mock.env(on, {})
4951
5052 return { reads, opened: opened.kept, clock }
5153 }
@@ -1,6 +1,7 @@
11 export * from './answers-of.js'
22 export * from './ask'
33 export * from './backend-of'
4+export * from './beneath'
45 export * from './big-lines.js'
56 export * from './branch-base-of'
67 export * from './builtin-holds.js'
@@ -9,10 +9,12 @@
99 export function oldFiles(on: On) {
1010 on('fs.list', () => ({
1111 value: [
12- { name: 'old.ts', kind: 'file', size: 2 },
13- { name: 'moved.ts', kind: 'file', size: 2 },
12+ { name: 'old.ts', kind: 'file', size: 2, isLink: false },
13+ { name: 'moved.ts', kind: 'file', size: 2, isLink: false },
1414 ],
1515 }))
1616
17- on('fs.stat', () => ({ value: { kind: 'file', size: 2, mtimeMs: 0 } }))
17+ on('fs.stat', () => ({
18+ value: { kind: 'file', size: 2, mtimeMs: 0, isLink: false },
19+ }))
1820 }
@@ -875,6 +875,187 @@
875875 expect(world.opened, 'the room it read for is gone').toEqual([])
876876 })
877877
878+ test("only the main loop's edit opens the pane", async ($, on) => {
879+ const world = Fixtures.inRepository(on)
880+
881+ const edit = (agentId?: string) =>
882+ $.tool.call({
883+ tool: 'Write',
884+ file_path: '/work/app.ts',
885+ content: '2',
886+ ...(agentId !== undefined && { agentId }),
887+ })
888+
889+ on('tool.call', () => ({ result: 'written' }))
890+
891+ await $.session.start(Fixtures.SESSION)
892+ await $.ui.render(Fixtures.HINT)
893+ await edit('a-explorer')
894+ await world.clock.advance(Fixtures.SETTLE_MS)
895+
896+ expect(world.opened, 'the built-in checkpoints no subagent edit').toEqual(
897+ [],
898+ )
899+
900+ expect(world.runs, 'so nothing was asked of the repository').toEqual([])
901+
902+ await edit()
903+ await world.clock.advance(Fixtures.SETTLE_MS)
904+
905+ expect(
906+ world.opened.map(pane => pane.id),
907+ "the main loop's",
908+ ).toEqual(['diff'])
909+ })
910+
911+ test('checkpointing off in settings: edits open nothing', async ($, on) => {
912+ const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
913+ settings: { fileCheckpointingEnabled: false },
914+ })
915+
916+ on('tool.call', () => ({ result: 'edited' }))
917+
918+ await $.session.start(Fixtures.SESSION)
919+ await $.ui.render(Fixtures.HINT)
920+
921+ await $.tool.call({
922+ tool: 'Edit',
923+ file_path: '/work/app.ts',
924+ old_string: '1',
925+ new_string: '2',
926+ })
927+
928+ await world.clock.advance(Fixtures.SETTLE_MS)
929+
930+ expect(world.opened, 'the built-in opens on a checkpoint alone').toEqual([])
931+
932+ expect(world.runs, 'so nothing was asked of the repository').toEqual([])
933+
934+ expect(await $.command.run(Fixtures.DIFF)).toEqual({
935+ text: 'Diff panel shown',
936+ })
937+ })
938+
939+ test('checkpointing off by variable: edits open nothing', async ($, on) => {
940+ const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
941+ env: { CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING: 'true' },
942+ })
943+
944+ on('tool.call', () => ({ result: 'edited' }))
945+
946+ await $.session.start(Fixtures.SESSION)
947+ await $.ui.render(Fixtures.HINT)
948+
949+ await $.tool.call({
950+ tool: 'Edit',
951+ file_path: '/work/app.ts',
952+ old_string: '1',
953+ new_string: '2',
954+ })
955+
956+ await world.clock.advance(Fixtures.SETTLE_MS)
957+
958+ expect(world.opened).toEqual([])
959+ })
960+
961+ test('an open left waiting is withdrawn; /diff opens it', async ($, on) => {
962+ let isNarrow = true
963+
964+ const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
965+ stored: { [Names.STORE_OPEN_KEY]: true },
966+ isLeftWaiting: () => isNarrow,
967+ })
968+
969+ const edit = () =>
970+ $.tool.call({
971+ tool: 'Edit',
972+ file_path: '/work/app.ts',
973+ old_string: '1',
974+ new_string: '2',
975+ })
976+
977+ on('tool.call', () => ({ result: 'edited' }))
978+
979+ await $.session.start(Fixtures.SESSION)
980+ await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
981+ await edit()
982+ await world.clock.advance(Fixtures.SETTLE_MS)
983+
984+ expect(
985+ world.waiting.map(pane => pane.id),
986+ 'the edit opened',
987+ ).toEqual(['diff'])
988+
989+ expect(
990+ world.closed.map(pane => pane.id),
991+ 'and withdrew the pane the engine left waiting, so no resize seats it',
992+ ).toEqual(['diff'])
993+
994+ expect(
995+ world.runs.map(run => Fixtures.gitWordOf(run.argv)),
996+ 'the repository was found and read once to decide, no hunks fetched ' +
997+ 'for a pane no one sees',
998+ ).toEqual([
999+ 'rev-parse --show-toplevel',
1000+ 'status',
1001+ 'diff --shortstat',
1002+ 'diff --numstat',
1003+ 'ls-files',
1004+ ])
1005+
1006+ isNarrow = false
1007+
1008+ expect(
1009+ await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)),
1010+ '/diff opens it, where a pane believed open would have been hidden',
1011+ ).toEqual({ text: 'Diff panel shown' })
1012+
1013+ await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
1014+ await edit()
1015+ await world.clock.advance(Fixtures.SETTLE_MS)
1016+
1017+ expect(
1018+ world.opened.map(pane => pane.id),
1019+ 'closed by hand, no edit opens it again',
1020+ ).toEqual(['diff'])
1021+ })
1022+
1023+ test('an unplaced open is tried again at the next edit', async ($, on) => {
1024+ let isNarrow = true
1025+
1026+ const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
1027+ stored: { [Names.STORE_OPEN_KEY]: true },
1028+ isLeftWaiting: () => isNarrow,
1029+ })
1030+
1031+ const edit = () =>
1032+ $.tool.call({
1033+ tool: 'Edit',
1034+ file_path: '/work/app.ts',
1035+ old_string: '1',
1036+ new_string: '2',
1037+ })
1038+
1039+ on('tool.call', () => ({ result: 'edited' }))
1040+
1041+ await $.session.start(Fixtures.SESSION)
1042+ await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
1043+ await edit()
1044+ await world.clock.advance(Fixtures.SETTLE_MS)
1045+
1046+ expect(world.opened, 'the first try waited and was withdrawn').toEqual([])
1047+
1048+ isNarrow = false
1049+
1050+ await edit()
1051+ await world.clock.advance(Fixtures.SETTLE_MS)
1052+
1053+ expect(
1054+ world.opened.map(pane => pane.id),
1055+ 'the next edit opened it, as the built-in reads the width again then',
1056+ ).toEqual(['diff'])
1057+ })
1058+
8781059 test('/clear closes the pane it finds open', async ($, on) => {
8791060 const world = Fixtures.inRepository(on)
8801061
@@ -955,7 +1136,7 @@
9551136
9561137 test('a worktree opens on the base it kept', async ($, on) => {
9571138 const world = Fixtures.inRepository(on, Fixtures.oneSecret(), {
958- 'base:/main/wt': 'uncommitted',
1139+ stored: { 'base:/main/wt': 'uncommitted' },
9591140 })
9601141
9611142 await $.session.start(Fixtures.WORKTREE_SESSION)
Large file, diff not shown.