After looking around a bit, I felt like subagent wasn't the right way to
introduce this tool to the model. And with some back and forth
verification + market research, we both agreed spawn_agent was a better
name for what this tool is doing.
It still calls the right tool if you ask for a subagent, but especially
if we move these to be running in the background, spawn will be a better
name anyway.
Release Notes:
- N/A
Previously we only had the ability to return anyhow errors, which
limited the amount of structured data we could send the model.
Now tools can also return structured data in the error cases, which is
super helpful for subagents in particular
Release Notes:
- N/A
Looking at this more, the map of tools is just for what is available and
all of the filtering happens at runtime. So we can just rely on the
current behavior for the list of tools (it was already a matching set)
and we can simplify all of the code paths again where we were adding
this filtered list.
Release Notes:
- N/A
There are some UI concerns, especially for permission requests, that get
extremely complicated for recurisve subagents.
We'll move this to 1 for now to reduce complexity, and revisit once we
have a better way for representing all current permission requests.
Release Notes:
- N/A
Removes tool filtering since this was throwing off certain models, and
also allows for more generic task prompts that don't always require
summaries. Since the models usually provide a wrap-up message, we don't
have to wait for another turn.
This also sets us up to allow the agent to re-interact with an existing
subagent thread.
Release Notes:
- N/A
---------
Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
Requests will be rate-limited by our internal logic, but it will "just"
work.
Release Notes:
- N/A
Add `agent_worktree_directory` to `GitSettings` for configuring where
agent worktrees are stored (default: Zed data dir). Remove `Copy` derive
from `GitSettings`/`GitContentSettings` (incompatible with String field)
and fix downstream `.as_ref().unwrap()` call sites.
Define `AgentGitWorktreeInfo` (branch, worktree_path, base_ref) and add
it to `DbThread` + `DbThreadMetadata` for persistence and session list
display.
Closes AI-33
Release Notes:
- N/A
This PR adds a button in the `git: branch diff` view that allows to
quickly and easily send the entire diff to your last used agent for a
review. What this does is automatically submits a (pre-written and
generic) prompt to the last agents you were using in the agent panel
with the whole content of your diff.
<img width="750" height="1964" alt="Screenshot 2026-02-18 at 3 35@2x"
src="https://github.com/user-attachments/assets/493d8cf4-4815-4b01-91a0-6a39ad6219fe"
/>
Release Notes:
- Added a "Review Branch" button in the `git: branch diff` view so that
the whole diff can be quickly sent for review to an agent.
---------
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Redirects to `/dev/null` (e.g. `2>/dev/null`, `&>/dev/null`) are
known-safe I/O routing, not commands. Previously, `extract_commands`
emitted normalized redirect strings like `"2> /dev/null"` as separate
entries in the command list checked against auto-allow regexes. Since
`check_commands` requires **all** extracted entries to match an allow
pattern, the unmatched redirect caused false-negatives — e.g. `git log
--oneline -20 2>/dev/null || echo ...` would not be auto-allowed despite
matching `^git` and `^echo` patterns.
Rather than removing all redirects from extraction (which would hide
dangerous redirects like `> /etc/passwd` from deny/confirm pattern
checking), this fix surgically skips only `/dev/null` targets during
redirect normalization. Redirects to real files are still emitted and
still require a matching pattern for auto-allow, preserving the
defense-in-depth property.
Closes AI-41
Release Notes:
- Fixed terminal auto-allow patterns incorrectly prompting for
confirmation on commands containing `/dev/null` redirects (e.g.
`2>/dev/null`).
This PR fixes issues with subagent cancellation, prior to this we would
still show a wait indicator for subagents that were cancelled from the
parent thread. We also removed the `stop_by_user` workaround, that code
path now uses `thread.cancel` directly
Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
Release Notes:
- N/A
---------
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Removes the feature flag around listing and loading existing sessions
for external agents.
Release Notes
- acp: Expose session history for all external agents that support it.
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Previously, clicking "Always allow for `cargo` commands" after running
`cargo build --release` would also silently permit `cargo run`
(arbitrary code execution), `cargo publish`, and any other cargo
subcommand. This was overly broad and did not match user intent.
Now the extracted pattern includes the subcommand when present, so the
button reads "Always allow for `cargo build` commands" and the pattern
`^cargo\s+build\b` only matches `cargo build` invocations — not `cargo
test`, `cargo run`, etc.
### How it works
- The second token is included in the pattern when it looks like a
subcommand (alphanumeric, hyphens, underscores, no leading `-`).
- When the second token is a flag (e.g. `ls -la`), only the command name
is used — the user sees "Always allow for `ls` commands".
- Single-word commands and path-like commands behave the same as before.
### Examples
| Command | Pattern | Button label |
|---|---|---|
| `cargo build --release` | `^cargo\s+build\b` | Always for `cargo
build` commands |
| `cargo test -p search` | `^cargo\s+test\b` | Always for `cargo test`
commands |
| `npm install` | `^npm\s+install\b` | Always for `npm install` commands
|
| `ls -la` | `^ls\b` | Always for `ls` commands |
| `ls` | `^ls\b` | Always for `ls` commands |
| `./script.sh` | *(rejected)* | *(no pattern button)* |
Release Notes:
- Agent: "Always allow" suggestions for terminal commands are now
subcommand-specific (e.g. "Always allow for `cargo build` commands"
instead of "Always allow for `cargo` commands").
Before you mark this PR as ready for review, make sure that you have:
- [ ] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
Release Notes:
- N/A
### Description
Related Discussions: #44499, #35742, #31851
Display cost multiplier for GitHub Copilot models in the model selectors
(Both in Chat Panel and Inline Assistant)
<img width="436" height="800" alt="image"
src="https://github.com/user-attachments/assets/c9ebd8fa-4d55-4be8-b3e1-f46dbf1f0145"
/>
### Some technical notes
Although this PR's primary intent is to show the cost multiplier for
GitHub Copilot models alone, I have included some necessary plumbing to
allow specifying costs for other providers in future. I have introduced
an enum called `LanguageModelCostInfo` for showing cost in different
ways for different models. Now, this enum is used in `LanguageModel`
trait to get the cost info.
For now to begin with, in `LanguageModelCostInfo`, I have specified two
ways of pricing: Request-based (1 Agent request - GitHub Copilot uses
this) and Token-based (1M Input tokens / 1M Output tokens). I had
initially thought about adding a `Free` type, especially for Ollama but
didn't do it after realizing that Ollama has paid plans. Right now, only
the Request-based pricing is implemented and used for Copilot models.
Feel free to suggest changes on how to improve this design better.
Release Notes:
- Show cost multiplier for GitHub Copilot models
---------
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Add StreamEnded variant so the client can distinguish between a stream
that the cloud ran to completion versus one that was interrupted (see
CLO-258). **That logic is to be added in a follow up PR**.
Add an Unknown fallback with #[serde(other)] for forward-compatible
deserialization of future variants.
The client advertises support via a new
x-zed-client-supports-stream-ended-request-completion-status header. The
server will only send the new variant if that header is passed. Both
StreamEnded and Unknown are silently ignored at the event mapping layer
(from_completion_request_status returns Ok(None)).
Part of CLO-264 and CLO-266; cloud-side changes to follow.
Release Notes:
- N/A
---------
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
It's possible that a given project/workspace has symlinks that go
outside the current directories. In order to alert the user of some
potential files the agent is accessing they may not be expecting, we ask
for permission first.
Release Notes:
- agent: Prompt for permission if the agent tries to access files whose
symlinks resolve outside the current workspace
When a tool's JSON response fails to parse, the system would:
1. Create a `LanguageModelToolResult` with the error
2. Add it to `pending_message.tool_results`
3. **Never add the corresponding `ToolUse` to
`pending_message.content`**
This left an orphaned `tool_result` that would be sent to the LLM API
without a matching `tool_use` block, causing the provider to reject the
entire request with an error like:
```
messages: Assistant message must contain at least one content block, if
immediately followed by a user message with tool_result
```
The issue was in `handle_tool_use_json_parse_error_event()`. It created
and returned a `LanguageModelToolResult` (which gets added to
`tool_results`), but **failed to add the corresponding `ToolUse` to the
message `content`**.
This asymmetry meant:
- `pending_message.content`: [] (empty - no ToolUse!)
- `pending_message.tool_results`: {id: result}
When `AgentMessage::to_request()` converted this to API messages, it
would create:
- Assistant message: no tool_use blocks ❌
- User message: tool_result block ✅
APIs require tool_use and tool_result to be paired, so this would fail.
**Without this fix, the conversation becomes permanently broken** -
every subsequent message in the thread fails with the same API error
because the orphaned tool_result remains in the message history. The
only recovery is to start a completely new conversation, making this a
particularly annoying bug for users.
Modified `handle_tool_use_json_parse_error_event()` to:
1. **Add the `ToolUse` to `pending_message.content`** before returning
the result
2. Parse the raw_input JSON (falling back to `{}` if invalid, as the API
requires an object)
3. Send the `tool_call` event to update the UI
4. Check for duplicates to avoid adding the same `tool_use` twice
This ensures `tool_use` and `tool_result` are always properly paired.
Added comprehensive test coverage for
`handle_tool_use_json_parse_error_event()`:
- ✅ Verifies tool_use is added to message content
- ✅ Confirms tool_use has correct metadata and JSON fallback
- ✅ Tests deduplication logic to prevent duplicates
- ✅ Validates JSON parsing for valid input
## Manual Testing
To reproduce and test the fix:
1. Install the test MCP server:
```bash
cargo install --git https://github.com/dastrobu/mcp-fail-server
```
3. Add to Zed settings to enable the server:
```json
{
"context_servers": {
"mcp-fail-server": {
"command": "mcp-fail-server",
"args":[]
}
}
}
```
4. Open the assistant panel and ask it to use the `fail` tool
5. Without the fix: The conversation breaks permanently - every
subsequent message fails with the same API error, forcing you to start a
new thread
<img width="399" height="531" alt="image"
src="https://github.com/user-attachments/assets/533bdf40-80d3-4726-a9d9-dbabbe7379e5"
/>
7. With the fix: The error is handled gracefully, displayed in the UI,
and the conversation remains usable
<img width="391" height="512" alt="image"
src="https://github.com/user-attachments/assets/73aa6767-eeac-4d5d-bf6f-1beccca1d5cb"
/>
The mcp-fail-server always returns an error, triggering the JSON parse
error path that previously caused orphaned tool_result blocks.
Release Notes:
- Fixed an issue where errors could occur in the agent panel if an LLM
emitted a tool call with an invalid JSON payload
---------
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Remove GPT-4o, GPT-4.1, GPT-4.1-mini, and o4-mini from BYOK model
options in Zed before OpenAI retires these models.
These models are being retired by OpenAI (ChatGPT workspace support ends
April 3, 2026), so they have been removed from the available models list
in Zed's BYOK provider.
Closes AI-4
Release Notes:
- Removed deprecated GPT-4o, GPT-4.1, GPT-4.1-mini, and o4-mini models
from OpenAI BYOK provider
Remove the `AgentsPanel` (from the `agent_ui_v2` crate) and the utility
pane infrastructure from the codebase.
The Agents Panel was a separate panel gated behind the `agent-v2`
feature flag that was redundant with the existing Agent Panel. Utility
panes were a layout concept (secondary panes next to the editor,
separate from dock panels) whose only consumer was `AgentThreadPane` in
the Agents Panel.
### Changes
- Deleted the entire `agent_ui_v2` crate (`agents_panel.rs`,
`agent_thread_pane.rs`)
- Deleted `workspace/src/utility_pane.rs`
- Removed `UtilityPane`, `UtilityPaneHandle`, `UtilityPanePosition`,
`MinimizePane`, `ClosePane` from `workspace/src/dock.rs`
- Removed all utility pane fields, methods, and render blocks from
`workspace.rs`
- Removed all aside toggle code from `pane.rs` and `pane_group.rs`
- Removed `agents_panel_dock` setting from agent settings and
`default.json`
- Removed all `agent_ui_v2` references from `main.rs`, `zed.rs`, and
Cargo.toml files
- Cleaned up test code in `tool_permissions.rs` and `agent_ui.rs`
Closes AI-17
(No release notes because this was all feature-flagged.)
Release Notes:
- N/A
This PR actually fixes two issues:
- the thread title being overridden even after it's been manually
edited; as you go and come back from the thread history view, the edited
title would get swapped for the auto-summarized one
- the parent thread title sometimes displaying the title of a subagent
thread; this would also override the manual edit
- - -
- [x] Tests
- [x] Code Reviewed
- [x] Manual QA
Release Notes:
- Agent: Fixed thread titles being overridden even when manually edited.
---------
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Release Notes:
- Fixed an issue where a request could fail if an MCP server with names
containing whitespace was used
## Summary
When multiple MCP servers expose tools with the same name, Zed
disambiguates them by prefixing the tool name with the server ID from
settings.json. If the server ID contains spaces or special characters
(e.g., `"Azure DevOps"`), the resulting tool name like `Azure
DevOps_echo` violates Anthropic's API pattern `^[a-zA-Z0-9_-]{1,128}$`,
causing API errors:
> "Received an error from the Anthropic API: tools.0.custom.name: String
should match pattern '^[a-zA-Z0-9_-]{1,128}$'"
## Solution
Convert server IDs to snake_case (using the `heck` crate already
available in the workspace) before using them as prefixes during tool
name disambiguation.
| Server ID in settings.json | Disambiguated Tool Name |
|---------------------------|------------------------|
| `"Azure DevOps"` | `azure_dev_ops_echo` |
| `"My MCP Server"` | `my_mcp_server_echo` |
## Test plan
- [x] Added test case for server name with spaces ("Azure DevOps") in
`test_mcp_tool_truncation`
- [x] Verified existing tests pass
- [x] Manually tested with two MCP servers having overlapping tool names
After (left), Before (right):
<img width="2880" height="1800" alt="Screenshot_20251228_163249"
src="https://github.com/user-attachments/assets/09c4e8f0-e282-4620-9db3-3e2c7d428d15"
/>
🤖 Generated with (some) help from [Claude
Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Previously, when an MCP server returned a tool response with
`is_error: true`, the error content was incorrectly treated as
a successful result. This could mislead the LLM into thinking
the tool call succeeded when it actually failed.
Now we check the `is_error` flag and propagate the error message
properly, allowing the agent to handle failures appropriately.
Release Notes:
- N/A
---------
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This patch updates the description of the `edit_file` tool to recommend
models to use the `move_path` tool instead of the `terminal` tool to
move or rename files. The `move_path` tool is more specific and shall be
preferred as it provides a better UI and dedicated permissions.
Release Notes:
- N/A
Closes AI-10
The thinking tool is no longer needed. This removes it from the agent
codebase and updates all tests to use other tools instead.
Release Notes:
- Removed the thinking tool from the Agent, as models now have their own
first-class thinking APIs.
When saving after a streaming edit, check if format-on-save is enabled
for the buffer's language and run formatting before saving.
(No release notes because we aren't using the streaming edit tool yet.)
Release Notes:
- N/A
Move shell command parsing logic (`extract_commands` and supporting
code) from the agent crate into a new `shell_command_parser` crate so it
can be reused by `agent_servers` for ACP permission checking.
Release Notes:
- N/A
TODO:
- [x] Review code
- [x] Decide whether to keep ignored API tests
Release Notes:
- Fixed a bug where cancelling a thread mid-thought would cause further
anthropic requests to fail
- Fixed a bug where the model configured on a thread would not be
persisted alongside that thread
This PR hardens the authorization flow for all file and directory tools.
## Sensitive settings protection
All file/directory tools (copy, move, create_directory, delete, save,
edit, streaming_edit) now detect and protect sensitive settings paths:
- Paths inside `.zed/` directories (local settings)
- Paths inside the global config directory (`~/.config/zed/` or
equivalent)
Even when the global default is `allow`, modifications to these paths
require explicit confirmation. The authorization dialog title is
annotated with "(local settings)" or "(settings)" to inform the user.
`sensitive_settings_kind` walks up ancestor directories to handle paths
where intermediate subdirectories don't exist yet (e.g.
`~/.config/zed/new_subdir/evil.json`).
## Deferred filesystem operations
Copy, move, create_directory, and delete tools now defer all
project/filesystem operations until after the user authorizes the
action. Previously, some tools began resolving project paths or
traversing directories before authorization.
## streaming_edit_file permissions
`streaming_edit_file` now shares `edit_file`'s tool name for permission
checks, ensuring consistent permission rules between the two edit tool
variants. The duplicated authorization logic is replaced by a shared
`authorize_file_edit` function.
## Copy/move pattern extraction
Copy and move tools now include both source and destination paths in
their permission context (`input_value`). The always-allow pattern is
extracted from the common parent directory of both paths, ensuring the
pattern covers future checks against both.
## Save tool improvements
- Authorization title now shows only the paths that need confirmation,
not all paths
- Title is annotated with "(local settings)" or "(settings)" for
sensitive paths
Release Notes:
- File and directory tool operations now require confirmation before
modifying sensitive settings paths.
Builds on top of #48620 to add explicit test coverage for the exact
bypass scenarios reported by users:
- `rm -rf /etc/../` — path traversal via single parent dir that
normalizes to `/`
- `rm -rf --no-preserve-root /` — long flag without `=value` that could
bypass the old regex
- `rm --no-preserve-root -rf /` — long flag positioned before short
flags
- `rm / -rf --no-preserve-root` — trailing long flag without `=value`
after the path operand
- `sudo rm -rf /`, `sudo rm -rf /*`, `sudo rm -rf --no-preserve-root /`
— sudo-prefixed variants
All of these cases are already correctly blocked by the hardened regex
patterns and path normalization logic added in #48620. These tests
confirm that the reported bypasses are addressed and guard against
regressions.
Release Notes:
- N/A
This PR improves the shell command parser to correctly handle I/O
redirects, here-documents, and compound command constructs. Previously,
commands hidden inside redirect targets, here-document bodies, or
function definition redirects could go undetected by the permission
system.
## Changes
- **Redirect handling**: Capture I/O redirects on simple commands,
compound commands (`if`/`while`/`for`/`case`/subshells/brace groups),
and function definitions
- **Nested command extraction**: Extract commands from redirect file
targets (e.g. `> $(dangerous_cmd)`) and here-document bodies
- **Bare redirect rejection**: Return `None` for bare redirects with no
command name (e.g. `> /etc/passwd`), which forces confirmation
- **Compound command redirects**: Apply redirects on compound commands
to their inner body commands
- **Fail-closed error handling**: `extract_commands_from_word` and
`normalize_word_piece_into` now return `None` on parse failures instead
of silently succeeding
- **Test coverage**: Extensive tests for quoted redirect targets,
no-space redirects, clobber operators, fd-to-fd redirects, pipe+redirect
combinations, function definition redirects, and more
Release Notes:
- Improved shell command parsing to correctly detect commands hidden in
I/O redirects, here-documents, and compound command constructs.
MCP tool names can contain colons (e.g. `mcp:server:tool`), which broke
the `splitn(3, ':')` parsing of always-allow/always-deny pattern option
IDs. This switches to newline (`\n`) as the delimiter between tool name
and pattern, since newlines cannot appear in either component.
## Changes
- **Option ID format**: Changed from
`always_allow_pattern:{tool}:{pattern}` to
`always_allow_pattern:{tool}\n{pattern}`
- **Response parsing**: Replaced `splitn(3, ':')` with `strip_prefix` +
`split_once('\n')`
- **Error logging**: Added `log::error!` when pattern parsing fails
(previously silent)
- **Tests**: Updated test assertions in `agent` and `agent_ui` crates
No release notes because granular tool permissions are still
feature-flagged.
Release Notes:
- N/A
This PR hardens the terminal tool's hardcoded security rules for
destructive commands like `rm -rf /`, and adds path normalization to
prevent traversal-based bypasses.
## Path normalization
Adds `normalize_path` which resolves `..`, `.`, and redundant path
separators, and `decide_permission_for_path` which checks permissions
against both raw and normalized paths (taking the most restrictive
result). This prevents attacks like `rm -rf /tmp/../../` which
previously bypassed the `rm -rf /` rule.
## rm command expansion
Adds `expand_rm_to_single_path_commands` which splits multi-argument rm
commands into individual single-path commands for checking. This catches
cases like `rm -rf /tmp /` where the dangerous path is the second
argument.
## Regex hardening
- **FLAGS**: Now accepts digits, underscores, and uppercase in long
flags (e.g. `--no-preserve-root`)
- **`--flag=value`**: Correctly matched as a single flag token
- **Trailing flags**: Handles GNU rm's acceptance of flags after path
operands (e.g. `rm / -rf`)
- **`--` marker**: Detects end-of-options bypass attempts (e.g. `rm -rf
-- /`)
- **Whitespace**: Handles tabs and other whitespace, not just spaces
## `$HOME`/`${HOME}` handling
Normalizes the suffix after `$HOME`/`${HOME}` variable references so
that traversal attacks like `rm -rf $HOME/./` or `rm -rf ${HOME}/foo/..`
are correctly detected.
Release Notes:
- Strengthened terminal security rules to detect path traversal attacks
in destructive commands like `rm -rf`.
The two `unbounded_send` calls in `ToolCallEventStream`'s authorize
methods were using `.ok()` to silently discard send failures. This meant
that if the authorization channel was closed, the tool call would hang
indefinitely waiting for a response that would never come.
## Changes
- Both `authorize_third_party_tool` and `authorize` methods now use `if
let Err(error)` to detect send failures
- On failure, logs the error with `log::error!` and returns
`Task::ready(Err(...))` so callers get immediate, meaningful feedback
Release Notes:
- Tool authorization failures are now logged and reported instead of
being silently ignored.
This PR updates the model selector to highlight the latest models that
are available through the Zed provider:
<img width="388" height="477" alt="Screenshot 2026-02-06 at 1 46 41 PM"
src="https://github.com/user-attachments/assets/70760399-ecf6-46e3-80a7-cb998216c192"
/>
Closes CLO-205.
Release Notes:
- Added a "Latest" indicator to highlight the latest models available
through the Zed provider.
In `streaming_edit_file_tool.rs`, edits are sorted in reverse order and
applied sequentially. If the LLM produces overlapping edit ranges, the
first applied edit shifts the buffer, and the second edit's range
targets stale offsets, leading to incorrect results.
This adds a `windows(2)` check on the sorted (descending by start) edits
that verifies each earlier-in-file range's end does not exceed the next
later-in-file range's start. The validation is done before entering the
buffer update closure so the error can propagate cleanly via
`anyhow::bail!`.
Release Notes:
- Fixed bug where streaming edits could apply edits incorrectly if the
model requested overlapping edit regions.
This PR removes the `fn name()` default method from the `AgentTool`
trait (which just returned `Self::NAME`) and replaces all call sites
with direct use of the `NAME` associated constant. This lets us use it
as a single source of truth in macros that want to access it at compile
time.
This PR also replaces hardcoded tool name string literals throughout the
codebase with the corresponding `NAME` constants, so tool name
references stay in sync if a tool is renamed.
Intentionally not changed:
- **Display strings** like `"Thinking"` and `"Subagent"` (user-facing UI
labels)
- **Serde attributes** like `#[serde(rename = "thinking")]` (wire
format)
- **The test assertion** `assert_eq!(SubagentTool::NAME, "subagent")`
(replacing the literal would make it tautological)
- **MCP server names** like `"mcp:srv:terminal"` (not tool identifiers)
Release Notes:
- N/A
## Problem
Zed's terminal permission system validates commands using regex patterns
against string representations of parsed commands. The
`SimpleCommand::to_string()` method in brush-parser preserves the raw
lexical form including shell quotes, causing pattern matching to fail on
obfuscated commands.
For example, all of these are semantically equivalent to `rm -rf /` in
POSIX shells, but produced different strings from `to_string()` that
bypassed the hardcoded deny pattern `rm\s+(-[rf]+\s+)*/\s*$`:
| Input | `extract_commands()` returned | Matched deny pattern? |
|---|---|---|
| `rm -rf /` | `"rm -rf /"` | ✅ Yes |
| `rm -rf '/'` | `"rm -rf '/'"` | ❌ No |
| `'rm' -rf /` | `"'rm' -rf /"` | ❌ No |
| `r'm' -rf /` | `"r'm' -rf /"` | ❌ No |
| `rm -r'f' /` | `"rm -r'f' /"` | ❌ No |
| `rm -rf \/` | `"rm -rf \\/"` | ❌ No |
Both hardcoded deny patterns (Phase 2, which operates on extracted
sub-commands) and user-configured `always_deny` patterns were affected.
## Fix
Replace the `simple_command.to_string()` call in
`extract_commands_from_simple_command` with manual construction that:
1. Iterates the command name (`word_or_name`) and word arguments from
the suffix
2. Normalizes each word by parsing it into `WordPiece`s via
`brush_parser::word::parse`
3. Reconstructs the semantic (unquoted) value from each piece:
- `Text` — used as-is
- `SingleQuotedText` — used as-is (brush-parser already strips the
wrapping quotes)
- `EscapeSequence` — leading `\` stripped to produce the unescaped
character
- `AnsiCQuotedText` — used as-is (brush-parser already evaluates
`$'...'`)
- `DoubleQuotedSequence` / `GettextDoubleQuotedSequence` — inner pieces
recursively normalized
- `TildePrefix` — preserved as `~` + suffix
- `ParameterExpansion` / `CommandSubstitution` /
`BackquotedCommandSubstitution` / `ArithmeticExpression` — original
source text preserved so patterns like `\$HOME` and `\${HOME}` continue
to match
4. Joins normalized words with spaces
Falls back to the raw `word.value` if word parsing fails.
## Cross-shell safety
The normalization operates on POSIX-parsed ASTs (brush-parser uses POSIX
grammar), so it applies POSIX quoting rules. For non-POSIX shells
(Nushell, PowerShell, Xonsh, Elvish, etc.) where quoting semantics
differ, any mismatch results in false positives (overly cautious
denials), never false negatives. This is the correct error direction for
a security feature. When brush-parser fails to parse a non-POSIX
command, `extract_commands` returns `None` and the existing fallback
(raw-string matching with `allow_enabled=false`) handles it.
(No release notes because granular tool permissions are still
feautre-flagged.)
Release Notes:
- N/A
Expands the hardcoded security rules that block dangerous `rm` commands
in the agent terminal tool.
### New blocked patterns
- `rm -rf $HOME` / `rm -rf $HOME/` / `rm -rf ${HOME}` / `rm -rf
${HOME}/`
- `rm -rf .` / `rm -rf ./`
- `rm -rf ..` / `rm -rf ../`
- `rm -rf ~/` (previously only `rm -rf ~` was blocked)
### Flag handling improvements
- Simplified the flag character class from `[rRfF]` to `[rf]` since the
regex is already compiled with case-insensitive mode — less confusing,
same behavior.
- Added tests verifying that reversed flags (`-fr`), uppercase (`RM
-RF`), split flags (`-r -f`), and chained commands all get caught.
### Safe commands still allowed
Paths like `rm -rf ./build`, `rm -rf ~/Documents`, `rm -rf
$HOME/Documents`, `rm -rf ../some_dir`, and `rm -rf .hidden_dir` are
**not** blocked.
Release Notes:
- Auto-block a wider range of agent terminal commands, e.g. `rm -rf
$HOME` in addition to `rm -rf ~`