fix(tab-configs): shell-escape worktree path prefix so spaces in repo names don't break git worktree add / cd (#11144)#11392
Conversation
|
I'm starting a first review of this pull request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This PR shell-escapes the static repo-derived prefix for generated worktree commands in build_tab_config and default worktree materialization, with unit coverage for spaces and shell metacharacters.
Concerns
⚠️ [IMPORTANT] The New Worktree modal path still appears to be unfixed:handle_new_worktree_submitbuilds its TOML viacrate::tab_configs::build_worktree_config_toml(...), whose commands still usegenerated_worktree_path_string(...)directly inapp/src/tab_configs/tab_config.rs. That is the+ > New worktreeflow and will still generate bare/.../My Project/...paths for repos with spaces or metacharacters, so #11144 is only partially fixed.⚠️ [IMPORTANT] This is a user-facing behavior change, but the PR description has no screenshot or screen recording demonstrating the fixed flow end to end. Please attach visual evidence for the generated worktree tab/config flow.
Verdict
Found: 0 critical, 2 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
…#11144 review) The previous patch covered `session_config::build_tab_config` and `materialize_default_worktree_config` but missed the `+ > New worktree` modal flow, which submits through `handle_new_worktree_submit` → `build_worktree_config_toml`. That function rendered the `git worktree add` / `cd` commands using the raw `generated_worktree_path_string(...)` output, so a repo path like `/Users/me/My Project` still produced a bare unquoted path that the user's shell split into separate positional args. Oz flagged this on PR warpdotdev#11392: > [IMPORTANT] The New Worktree modal path still appears to be unfixed: > `handle_new_worktree_submit` builds its TOML via > `crate::tab_configs::build_worktree_config_toml(...)`, whose commands > still use `generated_worktree_path_string(...)` directly. Promote `build_shell_safe_worktree_path` to a `pub(crate)` helper in `tab_config.rs` (next to `generated_worktree_path_string`, where it naturally lives) and route both call sites in `build_worktree_config_toml` through it. `session_config.rs` switches to the shared helper, so the escape policy lives in exactly one place. Adds 5 regression tests in `tab_config_tests::build_worktree_config_toml_path_quoting`: spaces, `$`, backticks, and a plain-path round-trip to guard against over-escaping. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Thanks for the catch — you're right, Addressed in
Added 5 regression tests in tab_config_tests::build_worktree_config_toml_path_quoting:
29/29 in Re: visual evidence — the New Worktree modal flow is now end-to-end safe; I'll attach a recording shortly demonstrating the modal authoring a worktree against a repo path with spaces. /oz-review |
|
I'm re-reviewing this pull request in response to a review request. You can view the conversation on Warp. I reviewed this pull request and requested human review from: Comment Powered by Oz |
Oz no longer requests changes for this pull request after the latest automated review.
There was a problem hiding this comment.
Overview
This PR updates the tab-config worktree command generation paths to shell-escape the repo-derived worktree path prefix while preserving the branch-name placeholders, with focused tests for spaces and shell metacharacters across both session-config and default worktree materialization flows.
Concerns
- No blocking correctness, security, or spec-alignment concerns found in the changed lines.
Verdict
Found: 0 critical, 0 important, 0 suggestions
Approve
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
moirahuang
left a comment
There was a problem hiding this comment.
we really appreciate you making a fix for this! as a high level comment, i think this approach won't work for Powershell because it needs backtick escaping, not backslashes
one possible approach we can explore is to execute git worktree add internally as structured process invocation, something like:
Command::new("git")
.arg("worktree")
.arg("add")
.arg("-b")
.arg(branch_name)
.arg(worktree_path)
.arg(base_branch)
do you mind exploring if that would be feasible please?
…arpdotdev#11144 review) The previous round hardcoded `ShellFamily::Posix` in `build_shell_safe_worktree_path` and `escape_worktree_path_prefix`, which ships backslash escapes (`/Users/me/My\ Project/<branch>`) for paths that contain shell metacharacters. PowerShell parses `\` as a literal character, not an escape — so a Windows user (or a PowerShell-on-Mac user) would still see the same shell-splitting bug, just with the escapes visible as garbage characters in the path. @moirahuang flagged this on PR warpdotdev#11392: > i think this approach won't work for Powershell because it needs > backtick escaping, not backslashes Thread a `shell_family: ShellFamily` parameter through the four worktree config builders: - `build_shell_safe_worktree_path` (tab_config.rs) - `build_worktree_config_toml` (tab_config.rs) - `build_tab_config` (session_config.rs) - `materialize_default_worktree_config` (user_config/mod.rs) - (and its private helper `escape_worktree_path_prefix`) All four call sites in `workspace/view.rs` now derive the shell family from `OperatingSystem::get().default_shell_family()` — POSIX backslash on macOS/Linux, PowerShell backtick on Windows. This matches the pattern already in use at `app/src/workflows/local_workflows.rs:169` for similar "build commands for the user's shell" code. Adds 2 new regression tests in `tab_configs::tab_config::tests::build_worktree_config_toml_path_quoting`: - `powershell_repo_name_with_space_uses_backtick_escape` — pins that a PowerShell session gets `My` Project` (backtick) instead of `My\ Project` (which PowerShell parses as a literal `\`). - `powershell_env_var_in_repo_name_is_backtick_escaped` — pins that `$env:VAR` in a repo path is backtick-escaped (otherwise PowerShell would expand it before `git worktree add` runs). Out of scope (follow-up): the rare edge case where a user's actual shell differs from their OS default (e.g. pwsh on macOS, or WSL bash on Windows). That requires resolving the user's preferred-shell setting at each call site, which adds noticeable plumbing without affecting the common path. A follow-up PR can route through `AvailableShells::get_user_preferred_shell` if maintainers want full coverage. 89/89 in `tab_configs` + 20/20 in `user_config` pass. clippy + fmt clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Thanks for the thoughtful read @moirahuang. After digging into the structured-invocation path, I realized the cleanest immediate fix is the same shape you flagged the lack of — per-shell escape via What's in the commitThreaded a
All four call sites in workspace/view.rs now derive the shell family from Why this shape and not full structured invocationI explored
Per-shell escape via TestsAdds 2 new PowerShell-specific regression tests in
|
There was a problem hiding this comment.
Thanks for submitting this fix! I left some inline comments with some changes.
More generally, the comments are too verbose. Please remove the excessive detail. Also, the configured line length limit in this project is 100 and your comments wrap at around 80. Please re-flow the comments on longer, fewer lines.
| /// in the rendered command as a bare `/Users/.../My Project/<branch>` and | ||
| /// the shell splits it into separate positional args, breaking | ||
| /// `git worktree add` (#11144). | ||
| pub(crate) fn build_shell_safe_worktree_path( |
There was a problem hiding this comment.
The fact that this has "safe" in the name is a code smell. With this change there is now a fn generated_worktree_path_string (the unsafe version) and fn build_shell_safe_worktree_path (the safe version). I'm concerned that new callers will call fn generated_worktree_path_string without realizing the risk. Ideally, we should have one obvious way to do it. Either merge these into a single function which will do the escaping, or remove the pub(crate) from the unsafe one so that callers will find the safe version. The name should not contain "safe" anymore. If for some reason we cannot merge these into a single function and need to keep the unsafe version, that should be named as such and contain something like "unsafe" or "unescaped" in the name.
There was a problem hiding this comment.
Oh also I just noticed there are 3 copies of fn generated_worktree_path_string in different files. I know that isn't your doing but can you clean that up while you're editing these files. That would be much appreciated! 🙏
| directory: &Path, | ||
| enable_worktree: bool, | ||
| autogenerate_worktree_branch_name: bool, | ||
| shell_family: warp_util::path::ShellFamily, |
There was a problem hiding this comment.
Making this a parameter is a subtle error. The TabConfig struct that this creates has a pane with shell: None. If the shell is None then we lookup AvailableShells::get_user_preferred_shell(ctx), the default shell configured by the user. This parameter has an unseen dependency that it must match the user's default shell. Callers can get this wrong.
Instead, we need to make sure we use the correct ShellFamily is chosen to escape the path.
| BRANCH_PLACEHOLDER, | ||
| ); | ||
| let worktree_path = | ||
| escape_worktree_path_prefix(&raw_worktree_path, BRANCH_PLACEHOLDER, shell_family); |
There was a problem hiding this comment.
I'm confused by this. You are building up a string only to immediately split it back apart? Can we avoid that unnecessary work?
| repo: &str, | ||
| base_branch: &str, | ||
| worktree_branch_name: Option<&str>, | ||
| shell_family: warp_util::path::ShellFamily, |
There was a problem hiding this comment.
| shell_family: warp_util::path::ShellFamily, | |
| shell_family: ShellFamily, |
Each instance of warp_util::path::ShellFamily should be replaced with an import and a shorter path.
| &selection.directory, | ||
| selection.enable_worktree, | ||
| selection.autogenerate_worktree_branch_name, | ||
| OperatingSystem::get().default_shell_family(), |
There was a problem hiding this comment.
This is an example of the subtle error I mentioned above. The OS's default shell could be different from the user-configured default shell.
…acharacters in repo names don't break `git worktree add` / `cd` (warpdotdev#11144) The "+ > New worktree config" sidebar flow generated commands like git worktree add -b turquoise-palo-verde /Users/x/.warp/worktrees/My Project/turquoise-palo-verde with the repo-derived path interpolated **without quoting or escaping**. The user's shell tokenized the path on the space, so `git worktree add` received four positional args and printed its `usage:` help instead of creating the worktree. No explicit error surfaced — the command appears to "complete" — so the failure is non-obvious. Fixes warpdotdev#11144. Two parallel call sites needed the same fix: * `app/src/user_config/mod.rs` — `materialize_default_worktree_config`, reached via the sidebar "+ > New worktree config" flow. Substitutes the repo-derived `worktree_path` into the rendered TOML commands. * `app/src/tab_configs/session_config.rs` — `build_tab_config`, the in-Rust-code worktree config builder used by the session-config modal. Same `format!`-into-command pattern. Both helpers now route the path through `ShellFamily::Posix.shell_escape` (the same primitive used by the cd / open-folder / cli-install / git checkout fixes — including PR warpdotdev#11132's grep / file_glob path quoting). The escape is applied only to the static repo-derived prefix; the `{{autogenerated_branch_name}}` / `{{worktree_branch_name}}` handlebars placeholder embedded in the path is left untouched so the tab-config render pass can still resolve it. To keep the change reviewable, each call site grew a small helper: * `escape_worktree_path_prefix(worktree_path, branch_placeholder)` in `user_config/mod.rs` * `build_shell_safe_worktree_path(directory, branch_placeholder)` in `tab_configs/session_config.rs` Both share the same logic shape: split the rendered path at the placeholder, escape the prefix, re-join. Falls back to escaping the whole path if the placeholder is somehow absent (defense in depth). ## Tests 11 new unit tests across the two existing sibling `_tests.rs` files, covering both code paths: * spaces in the repo name (the original report) * `$` in the repo name (would otherwise be expanded as a variable) * backtick in the repo name (would otherwise trigger command substitution) * handlebars placeholder is preserved through the escape * plain paths remain unchanged (regression guard against over-escaping) * the named-branch variant of `build_tab_config` (uses the same helper) ``` $ cargo nextest run -p warp -E 'test(worktree_path_quoting::)' 11 tests run: 11 passed $ cargo nextest run -p warp -E 'test(tab_configs::) or test(user_config::)' 91 tests run: 91 passed (no regression in existing tests) $ cargo clippy -p warp --tests -- -D warnings clean $ cargo fmt -p warp -- --check clean ``` Sibling PRs in the same shell-escape family: warpdotdev#11391 (warpdotdev#11132) covers the agent grep / file_glob / is_file_path helpers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#11144 review) The previous patch covered `session_config::build_tab_config` and `materialize_default_worktree_config` but missed the `+ > New worktree` modal flow, which submits through `handle_new_worktree_submit` → `build_worktree_config_toml`. That function rendered the `git worktree add` / `cd` commands using the raw `generated_worktree_path_string(...)` output, so a repo path like `/Users/me/My Project` still produced a bare unquoted path that the user's shell split into separate positional args. Oz flagged this on PR warpdotdev#11392: > [IMPORTANT] The New Worktree modal path still appears to be unfixed: > `handle_new_worktree_submit` builds its TOML via > `crate::tab_configs::build_worktree_config_toml(...)`, whose commands > still use `generated_worktree_path_string(...)` directly. Promote `build_shell_safe_worktree_path` to a `pub(crate)` helper in `tab_config.rs` (next to `generated_worktree_path_string`, where it naturally lives) and route both call sites in `build_worktree_config_toml` through it. `session_config.rs` switches to the shared helper, so the escape policy lives in exactly one place. Adds 5 regression tests in `tab_config_tests::build_worktree_config_toml_path_quoting`: spaces, `$`, backticks, and a plain-path round-trip to guard against over-escaping. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…arpdotdev#11144 review) The previous round hardcoded `ShellFamily::Posix` in `build_shell_safe_worktree_path` and `escape_worktree_path_prefix`, which ships backslash escapes (`/Users/me/My\ Project/<branch>`) for paths that contain shell metacharacters. PowerShell parses `\` as a literal character, not an escape — so a Windows user (or a PowerShell-on-Mac user) would still see the same shell-splitting bug, just with the escapes visible as garbage characters in the path. @moirahuang flagged this on PR warpdotdev#11392: > i think this approach won't work for Powershell because it needs > backtick escaping, not backslashes Thread a `shell_family: ShellFamily` parameter through the four worktree config builders: - `build_shell_safe_worktree_path` (tab_config.rs) - `build_worktree_config_toml` (tab_config.rs) - `build_tab_config` (session_config.rs) - `materialize_default_worktree_config` (user_config/mod.rs) - (and its private helper `escape_worktree_path_prefix`) All four call sites in `workspace/view.rs` now derive the shell family from `OperatingSystem::get().default_shell_family()` — POSIX backslash on macOS/Linux, PowerShell backtick on Windows. This matches the pattern already in use at `app/src/workflows/local_workflows.rs:169` for similar "build commands for the user's shell" code. Adds 2 new regression tests in `tab_configs::tab_config::tests::build_worktree_config_toml_path_quoting`: - `powershell_repo_name_with_space_uses_backtick_escape` — pins that a PowerShell session gets `My` Project` (backtick) instead of `My\ Project` (which PowerShell parses as a literal `\`). - `powershell_env_var_in_repo_name_is_backtick_escaped` — pins that `$env:VAR` in a repo path is backtick-escaped (otherwise PowerShell would expand it before `git worktree add` runs). Out of scope (follow-up): the rare edge case where a user's actual shell differs from their OS default (e.g. pwsh on macOS, or WSL bash on Windows). That requires resolving the user's preferred-shell setting at each call site, which adds noticeable plumbing without affecting the common path. A follow-up PR can route through `AvailableShells::get_user_preferred_shell` if maintainers want full coverage. 89/89 in `tab_configs` + 20/20 in `user_config` pass. clippy + fmt clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses @acarl005's warpdotdev#11392 review: - AvailableShells gains user_preferred_shell_family(ctx), the single source of truth for the family a `shell: None` pane will execute under. workspace/view.rs callers (build_tab_config, build_worktree_config_toml, materialize_default_worktree_config) now read from it instead of OperatingSystem::default_shell_family() — so a user who runs PowerShell on macOS or pwsh on Linux gets the right escape regardless of OS default. - Docstrings on build_shell_safe_worktree_path, build_tab_config, and build_worktree_config_toml now state the precondition explicitly and point at the helper, so future callers can't repeat the mistake. - materialize_default_worktree_config no longer builds a worktree path string just to split it back apart at the placeholder — it calls build_shell_safe_worktree_path directly. Drops the sibling escape_worktree_path_prefix helper entirely. - generated_worktree_path_string is now pub(super) so external callers must go through the escaped helper. - warp_util::path::ShellFamily is imported once per file via `use ShellFamily;` instead of fully-qualified at every signature. - Test wrappers in tab_config_tests / session_config_tests renamed to expected_worktree_path to remove the name-collision with the canonical helper. Test coverage: 5 new tests on NewSessionShell::shell_family pinning that pwsh/powershell binaries (on any platform, in any of Executable / MSYS2 / Custom variants) resolve to ShellFamily::PowerShell. 89 tab_configs + 20 user_config existing tests still pass. clippy + fmt clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
31d7972 to
dfb974d
Compare
|
Thanks Andy — addressed all six in `dfb974d4`. Quick rundown: (2 + 5) Hidden dependency on user-preferred shell. Added (3) Build-a-string-then-split. (1 + 6) Footgun + 3 copies of (4) Short import. Tests: 5 new on Rebased onto current master while I was in there. Let me know if you'd rather see any of the docstring or naming tweaked. |
Description
The "+ > New worktree config" sidebar flow generated commands like
with the repo-derived path interpolated without quoting or escaping. The user's shell tokenized the path on the space, so
git worktree addreceived four positional args and printed itsusage:help instead of creating the worktree. No explicit error surfaced — the command appears to "complete" — so the failure is non-obvious.Fixes #11144.
Root cause
Two parallel call sites embed the repo-derived path into a generated command, neither one escaped:
app/src/user_config/mod.rs:250-253materialize_default_worktree_configlet worktree_path = generated_worktree_path_string(...)then string-substituted into the command templateapp/src/tab_configs/session_config.rs:118-122build_tab_config(autogenerated branch)format!("git worktree add -b {autogenerated_branch_name} {worktree_path}")app/src/tab_configs/session_config.rs:125-129build_tab_config(named branch)format!("git worktree add -b {worktree_branch_name} {worktree_path}")Both produce a TabConfig whose worktree commands embed a bare unescaped
worktree_path. The user's shell parses that path verbatim, so any space /$/ backtick /(/)in the path component breaks the command.Fix
Both call sites now route the path through
ShellFamily::Posix.shell_escape(the same primitive used by the cd / open-folder / cli-install / git-checkout fixes — including PR #11391 (#11132) for the sibling grep / file_glob path quoting). The escape is applied only to the static repo-derived prefix; the{{autogenerated_branch_name}}/{{worktree_branch_name}}handlebars placeholder embedded in the path is left untouched so the tab-config render pass can still resolve it.Each call site grew a small helper:
escape_worktree_path_prefix(worktree_path, branch_placeholder)inuser_config/mod.rsbuild_shell_safe_worktree_path(directory, branch_placeholder)intab_configs/session_config.rsBoth share the same logic shape: split the rendered path at the placeholder, escape the prefix, re-join. Falls back to escaping the whole path if the placeholder is somehow absent (defense in depth).
Shell-level before / after
Testing
11 new unit tests across the two existing sibling
_tests.rsfiles cover both code paths:/Users/luizv/Developer/2026-05 Site da Jô)$in the repo name (would otherwise be expanded as a variable)build_tab_config(uses the same helper)Relationship to #11132 / #11391
This fix is part of the same shell-escape cluster as PR #11391 (#11132), which covers the agent's grep / file_glob /
is_file_path/is_git_repositoryhelpers. The two PRs touch disjoint files (tab_configs/+user_config/here vs.ai/blocklist/action_model/execute/there) and ship independently. Both use the samewarp_util::path::ShellFamily::shell_escapeprimitive.Server API dependencies
None — this is client-only.
Agent Mode
Unchecked (external contribution).
Changelog
CHANGELOG-BUG-FIX: "+ > New worktree config" no longer breaks when the repository's directory name contains spaces or shell metacharacters. The generated git worktree add / cd commands now shell-escape the path.Fixes #11144