diff --git a/.claude-plugin/skills/worktrunk/reference/hook.md b/.claude-plugin/skills/worktrunk/reference/hook.md
index a75818d8e..ade972d1e 100644
--- a/.claude-plugin/skills/worktrunk/reference/hook.md
+++ b/.claude-plugin/skills/worktrunk/reference/hook.md
@@ -202,14 +202,14 @@ Many tasks work well in `post-start` — they'll likely be ready by the time the
### Copying untracked files
-Git worktrees share the repository but not untracked files (dependencies, caches, `.env`). Use [`wt step copy-ignored`](https://worktrunk.dev/step/#wt-step-copy-ignored) to copy files listed in `.worktreeinclude`:
+Git worktrees share the repository but not untracked files (dependencies, caches, `.env`). Use [`wt step copy-ignored`](https://worktrunk.dev/step/#wt-step-copy-ignored) to copy gitignored files:
```toml
[post-create]
copy = "wt step copy-ignored"
```
-See [`wt step copy-ignored`](https://worktrunk.dev/step/#wt-step-copy-ignored) for setup, common patterns, and language-specific notes.
+See [`wt step copy-ignored`](https://worktrunk.dev/step/#wt-step-copy-ignored) for limiting what gets copied, common patterns, and language-specific notes.
### Dev servers
diff --git a/.claude-plugin/skills/worktrunk/reference/step.md b/.claude-plugin/skills/worktrunk/reference/step.md
index 17d2392fb..f15356189 100644
--- a/.claude-plugin/skills/worktrunk/reference/step.md
+++ b/.claude-plugin/skills/worktrunk/reference/step.md
@@ -26,7 +26,7 @@ wt step push
- `squash` — Squash all branch commits into one with [LLM-generated message](https://worktrunk.dev/llm-commits/)
- `rebase` — Rebase onto target branch
- `push` — Fast-forward target to current branch
-- `copy-ignored` — Copy files listed in `.worktreeinclude`
+- `copy-ignored` — Copy gitignored files between worktrees
- `for-each` — [experimental] Run a command in every worktree
## Options
@@ -76,7 +76,7 @@ Usage: wt step [OPTIONS] squash Squash commits since branching
push Fast-forward target to current branch
rebase Rebase onto target
- copy-ignored Copy .worktreeinclude files to another worktree
+ copy-ignored Copy gitignored files to another worktree
for-each [experimental] Run command in each worktree
Options:
@@ -95,20 +95,10 @@ Usage: wt step [OPTIONS] .worktreeinclude files to another worktree
+wt step copy-ignored - Copy gitignored files to another worktree
-Copies files listed in .worktreeinclude that are also gitignored. Useful in
-post-create hooks to sync local config files (.env, IDE settings) to new
-worktrees. Skips symlinks and existing files.
+Copies gitignored files to another worktree. By default copies all gitignored
+files; use .worktreeinclude to limit what gets copied. Useful in post-create
+hooks to sync local config files (.env, IDE settings) to new worktrees. Skips
+symlinks and existing files.
Usage: wt step copy-ignored [OPTIONS]
diff --git a/docs/content/hook.md b/docs/content/hook.md
index 89d94648c..e45e58a5d 100644
--- a/docs/content/hook.md
+++ b/docs/content/hook.md
@@ -210,14 +210,14 @@ Many tasks work well in `post-start` — they'll likely be ready by the time the
### Copying untracked files
-Git worktrees share the repository but not untracked files (dependencies, caches, `.env`). Use [`wt step copy-ignored`](@/step.md#wt-step-copy-ignored) to copy files listed in `.worktreeinclude`:
+Git worktrees share the repository but not untracked files (dependencies, caches, `.env`). Use [`wt step copy-ignored`](@/step.md#wt-step-copy-ignored) to copy gitignored files:
```toml
[post-create]
copy = "wt step copy-ignored"
```
-See [`wt step copy-ignored`](@/step.md#wt-step-copy-ignored) for setup, common patterns, and language-specific notes.
+See [`wt step copy-ignored`](@/step.md#wt-step-copy-ignored) for limiting what gets copied, common patterns, and language-specific notes.
### Dev servers
diff --git a/docs/content/step.md b/docs/content/step.md
index 055985259..d4581e125 100644
--- a/docs/content/step.md
+++ b/docs/content/step.md
@@ -34,7 +34,7 @@ wt step push
- `squash` — Squash all branch commits into one with [LLM-generated message](@/llm-commits.md)
- `rebase` — Rebase onto target branch
- `push` — Fast-forward target to current branch
-- `copy-ignored` — Copy files listed in `.worktreeinclude`
+- `copy-ignored` — Copy gitignored files between worktrees
- `for-each` — [experimental] Run a command in every worktree
## Options
@@ -90,7 +90,7 @@ Usage: wt step [OPTIONS] squash Squash commits since branching
push Fast-forward target to current branch
rebase Rebase onto target
- copy-ignored Copy .worktreeinclude files to another worktree
+ copy-ignored Copy gitignored files to another worktree
for-each [experimental] Run command in each worktree
Options:
@@ -110,20 +110,10 @@ Usage: wt step [OPTIONS] .worktreeinclude files to another worktree
+wt step copy-ignored - Copy gitignored files to another worktree
-Copies files listed in .worktreeinclude that are also gitignored. Useful in
-post-create hooks to sync local config files (.env, IDE settings) to new
-worktrees. Skips symlinks and existing files.
+Copies gitignored files to another worktree. By default copies all gitignored
+files; use .worktreeinclude to limit what gets copied. Useful in post-create
+hooks to sync local config files (.env, IDE settings) to new worktrees. Skips
+symlinks and existing files.
Usage: wt step copy-ignored [OPTIONS]
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index fe7d73e65..65af569e6 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -935,7 +935,7 @@ wt step push
- `squash` — Squash all branch commits into one with [LLM-generated message](@/llm-commits.md)
- `rebase` — Rebase onto target branch
- `push` — Fast-forward target to current branch
-- `copy-ignored` — Copy files listed in `.worktreeinclude`
+- `copy-ignored` — Copy gitignored files between worktrees
- `for-each` — [experimental] Run a command in every worktree
## Options
@@ -1194,14 +1194,14 @@ Many tasks work well in `post-start` — they'll likely be ready by the time the
### Copying untracked files
-Git worktrees share the repository but not untracked files (dependencies, caches, `.env`). Use [`wt step copy-ignored`](@/step.md#wt-step-copy-ignored) to copy files listed in `.worktreeinclude`:
+Git worktrees share the repository but not untracked files (dependencies, caches, `.env`). Use [`wt step copy-ignored`](@/step.md#wt-step-copy-ignored) to copy gitignored files:
```toml
[post-create]
copy = "wt step copy-ignored"
```
-See [`wt step copy-ignored`](@/step.md#wt-step-copy-ignored) for setup, common patterns, and language-specific notes.
+See [`wt step copy-ignored`](@/step.md#wt-step-copy-ignored) for limiting what gets copied, common patterns, and language-specific notes.
### Dev servers
diff --git a/src/cli/step.rs b/src/cli/step.rs
index 80d112729..f6461a9c8 100644
--- a/src/cli/step.rs
+++ b/src/cli/step.rs
@@ -76,27 +76,17 @@ pub enum StepCommand {
target: Option,
},
- /// Copy `.worktreeinclude` files to another worktree
+ /// Copy gitignored files to another worktree
///
- /// Copies files listed in `.worktreeinclude` that are also gitignored.
- /// Useful in post-create hooks to sync local config files
- /// (`.env`, IDE settings) to new worktrees. Skips symlinks and existing
- /// files.
+ /// Copies gitignored files to another worktree. By default copies all
+ /// gitignored files; use `.worktreeinclude` to limit what gets copied.
+ /// Useful in post-create hooks to sync local config files (`.env`, IDE
+ /// settings) to new worktrees. Skips symlinks and existing files.
#[command(
- after_long_help = r#"Git worktrees share the repository but not untracked files. This command copies files listed in `.worktreeinclude` to another worktree, eliminating cold starts.
+ after_long_help = r#"Git worktrees share the repository but not untracked files. This command copies gitignored files to another worktree, eliminating cold starts.
## Setup
-Create a `.worktreeinclude` file in your repository root listing patterns to copy (uses gitignore syntax):
-
-```gitignore
-# .worktreeinclude
-.env
-node_modules/
-target/
-.cache/
-```
-
Add to your project config:
```toml
@@ -105,9 +95,19 @@ Add to your project config:
copy = "wt step copy-ignored"
```
+All gitignored files are copied by default, as if `.worktreeinclude` contained `**`. To copy only specific patterns, create a `.worktreeinclude` file using gitignore syntax:
+
+```gitignore
+# .worktreeinclude — optional, limits what gets copied
+.env
+node_modules/
+target/
+.cache/
+```
+
## What gets copied
-Files are copied only if they match **both** `.worktreeinclude` **and** are gitignored. This prevents accidentally copying tracked files.
+Only gitignored files are copied — tracked files are never touched. If `.worktreeinclude` exists, files must match **both** `.worktreeinclude` **and** be gitignored.
## Common patterns
diff --git a/src/commands/config.rs b/src/commands/config.rs
index 2157f740c..db819466b 100644
--- a/src/commands/config.rs
+++ b/src/commands/config.rs
@@ -981,7 +981,10 @@ pub fn handle_state_get(key: &str, refresh: bool, branch: Option) -> any
.unwrap_or_default();
if head.is_empty() {
- anyhow::bail!("Branch '{branch_name}' not found");
+ return Err(worktrunk::git::GitError::InvalidReference {
+ reference: branch_name,
+ }
+ .into());
}
if refresh {
diff --git a/src/commands/step_commands.rs b/src/commands/step_commands.rs
index 86be0a022..29c08da80 100644
--- a/src/commands/step_commands.rs
+++ b/src/commands/step_commands.rs
@@ -378,7 +378,7 @@ pub fn handle_rebase(target: Option<&str>) -> anyhow::Result {
}
// Not a rebase conflict, return original error
return Err(worktrunk::git::GitError::Other {
- message: format!("Failed to rebase onto '{}': {}", target_branch, e),
+ message: cformat!("Failed to rebase onto {}>: {}", target_branch, e),
}
.into());
}
@@ -409,9 +409,11 @@ pub fn handle_rebase(target: Option<&str>) -> anyhow::Result {
/// Handle `wt step copy-ignored` command
///
-/// Copies files matching the intersection of `.worktreeinclude` and gitignored files
-/// from a source worktree to a destination worktree. Uses COW (reflink) when
-/// available for efficient copying of large directories like `target/`.
+/// Copies gitignored files from a source worktree to a destination worktree.
+/// If a `.worktreeinclude` file exists, only files matching both `.worktreeinclude`
+/// and gitignore patterns are copied. Without `.worktreeinclude`, all gitignored
+/// files are copied. Uses COW (reflink) when available for efficient copying of
+/// large directories like `target/`.
pub fn step_copy_ignored(
from: Option<&str>,
to: Option<&str>,
@@ -425,18 +427,22 @@ pub fn step_copy_ignored(
// Resolve source and destination worktree paths
let (source_path, source_context) = match from {
Some(branch) => {
- let path = repo
- .worktree_for_branch(branch)?
- .ok_or_else(|| anyhow::anyhow!("No worktree found for branch '{}'", branch))?;
+ let path = repo.worktree_for_branch(branch)?.ok_or_else(|| {
+ worktrunk::git::GitError::WorktreeNotFound {
+ branch: branch.to_string(),
+ }
+ })?;
(path, branch.to_string())
}
None => (repo.worktree_base()?, repo.default_branch()?.to_string()),
};
let dest_path = match to {
- Some(branch) => repo
- .worktree_for_branch(branch)?
- .ok_or_else(|| anyhow::anyhow!("No worktree found for branch '{}'", branch))?,
+ Some(branch) => repo.worktree_for_branch(branch)?.ok_or_else(|| {
+ worktrunk::git::GitError::WorktreeNotFound {
+ branch: branch.to_string(),
+ }
+ })?,
None => repo.worktree_root()?.to_path_buf(),
};
@@ -445,34 +451,33 @@ pub fn step_copy_ignored(
return Ok(());
}
- // Check for .worktreeinclude file
- let include_path = source_path.join(".worktreeinclude");
- if !include_path.exists() {
- crate::output::print(info_message(cformat!(
- "No .worktreeinclude> file found"
- )))?;
- return Ok(());
- }
-
// Get ignored entries from git
// --directory stops at directory boundaries (avoids listing thousands of files in target/)
let ignored_entries = list_ignored_entries(&source_path, &source_context)?;
- // Build include matcher from .worktreeinclude
- let include_matcher = {
- let mut builder = GitignoreBuilder::new(&source_path);
- if let Some(err) = builder.add(&include_path) {
- return Err(anyhow::anyhow!("Error parsing .worktreeinclude: {}", err));
- }
- builder.build().context("Failed to build include matcher")?
+ // Filter to entries that match .worktreeinclude (or all if no file exists)
+ let include_path = source_path.join(".worktreeinclude");
+ let entries_to_copy: Vec<_> = if include_path.exists() {
+ // Build include matcher from .worktreeinclude
+ let include_matcher = {
+ let mut builder = GitignoreBuilder::new(&source_path);
+ if let Some(err) = builder.add(&include_path) {
+ return Err(worktrunk::git::GitError::WorktreeIncludeParseError {
+ error: err.to_string(),
+ }
+ .into());
+ }
+ builder.build().context("Failed to build include matcher")?
+ };
+ ignored_entries
+ .into_iter()
+ .filter(|(path, is_dir)| include_matcher.matched(path, *is_dir).is_ignore())
+ .collect()
+ } else {
+ // No .worktreeinclude file — default to copying all ignored entries
+ ignored_entries
};
- // Filter to entries that match .worktreeinclude
- let entries_to_copy: Vec<_> = ignored_entries
- .into_iter()
- .filter(|(path, is_dir)| include_matcher.matched(path, *is_dir).is_ignore())
- .collect();
-
if entries_to_copy.is_empty() {
crate::output::print(info_message("No matching files to copy"))?;
return Ok(());
diff --git a/src/git/error.rs b/src/git/error.rs
index feb93c747..586454792 100644
--- a/src/git/error.rs
+++ b/src/git/error.rs
@@ -131,6 +131,9 @@ pub enum GitError {
ParseError {
message: String,
},
+ WorktreeIncludeParseError {
+ error: String,
+ },
LlmCommandFailed {
command: String,
error: String,
@@ -140,6 +143,9 @@ pub enum GitError {
ProjectConfigNotFound {
config_path: PathBuf,
},
+ WorktreeNotFound {
+ branch: String,
+ },
Other {
message: String,
},
@@ -545,6 +551,19 @@ impl std::fmt::Display for GitError {
write!(f, "{}", error_message(message))
}
+ GitError::WorktreeIncludeParseError { error } => {
+ let header = error_message(cformat!("Error parsing .worktreeinclude>"));
+ write!(f, "{}", format_error_block(header, error))
+ }
+
+ GitError::WorktreeNotFound { branch } => {
+ write!(
+ f,
+ "{}",
+ error_message(cformat!("No worktree found for branch {branch}>"))
+ )
+ }
+
GitError::Other { message } => {
write!(f, "{}", error_message(message))
}
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index def46cc35..4be3a6e9b 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -552,7 +552,10 @@ pub fn configure_cli_command(cmd: &mut Command) {
cmd.env("WORKTRUNK_CONFIG_PATH", "/nonexistent/test/config.toml");
cmd.env("CLICOLOR_FORCE", "1");
cmd.env("SOURCE_DATE_EPOCH", TEST_EPOCH.to_string());
- cmd.env("COLUMNS", "150");
+ // Use wide terminal to prevent wrapping differences across platforms.
+ // macOS temp paths (~80 chars) are much longer than Linux (~10 chars),
+ // so error messages containing paths need room to avoid platform-specific line breaks.
+ cmd.env("COLUMNS", "500");
// Set consistent terminal type for hyperlink detection via supports-hyperlinks crate
cmd.env("TERM", "alacritty");
// Enable warn-level logging so diagnostics show up in test failures
diff --git a/tests/integration_tests/step_copy_ignored.rs b/tests/integration_tests/step_copy_ignored.rs
index dd495d8d2..0b293efa1 100644
--- a/tests/integration_tests/step_copy_ignored.rs
+++ b/tests/integration_tests/step_copy_ignored.rs
@@ -5,11 +5,57 @@ use insta_cmd::assert_cmd_snapshot;
use rstest::rstest;
use std::fs;
-/// Test with no .worktreeinclude file
+/// Test with no .worktreeinclude file and no gitignored files
#[rstest]
fn test_copy_ignored_no_worktreeinclude(mut repo: TestRepo) {
let feature_path = repo.add_worktree("feature");
- // No .worktreeinclude file exists
+ // No .worktreeinclude file and no gitignored files → nothing to copy
+ assert_cmd_snapshot!(make_snapshot_cmd(
+ &repo,
+ "step",
+ &["copy-ignored"],
+ Some(&feature_path),
+ ));
+}
+
+/// Test default behavior: copies all gitignored files when no .worktreeinclude exists
+#[rstest]
+fn test_copy_ignored_default_copies_all(mut repo: TestRepo) {
+ let feature_path = repo.add_worktree("feature");
+
+ // Create gitignored files but NO .worktreeinclude
+ fs::write(repo.root_path().join(".env"), "SECRET=value").unwrap();
+ fs::write(repo.root_path().join("cache.db"), "cached data").unwrap();
+ fs::write(repo.root_path().join(".gitignore"), ".env\ncache.db\n").unwrap();
+
+ // Without .worktreeinclude, all gitignored files should be copied
+ assert_cmd_snapshot!(make_snapshot_cmd(
+ &repo,
+ "step",
+ &["copy-ignored"],
+ Some(&feature_path),
+ ));
+
+ // Verify both files were copied
+ assert!(
+ feature_path.join(".env").exists(),
+ ".env should be copied without .worktreeinclude"
+ );
+ assert!(
+ feature_path.join("cache.db").exists(),
+ "cache.db should be copied without .worktreeinclude"
+ );
+}
+
+/// Test error handling when .worktreeinclude has invalid syntax
+#[rstest]
+fn test_copy_ignored_invalid_worktreeinclude(mut repo: TestRepo) {
+ let feature_path = repo.add_worktree("feature");
+
+ // Create invalid .worktreeinclude (unclosed brace in alternate group)
+ fs::write(repo.root_path().join(".worktreeinclude"), "{unclosed\n").unwrap();
+
+ // Should fail with parse error
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
@@ -448,3 +494,43 @@ fn test_copy_ignored_to_flag(mut repo: TestRepo) {
"from-main"
);
}
+
+/// Test --from with a branch that has no worktree
+#[rstest]
+fn test_copy_ignored_from_nonexistent_worktree(repo: TestRepo) {
+ // Create a branch without a worktree
+ repo.git_command()
+ .args(["branch", "orphan-branch"])
+ .output()
+ .unwrap();
+
+ // Try to copy from a branch with no worktree
+ assert_cmd_snapshot!(make_snapshot_cmd(
+ &repo,
+ "step",
+ &["copy-ignored", "--from", "orphan-branch"],
+ None,
+ ));
+}
+
+/// Test --to with a branch that has no worktree
+#[rstest]
+fn test_copy_ignored_to_nonexistent_worktree(repo: TestRepo) {
+ // Create a branch without a worktree
+ repo.git_command()
+ .args(["branch", "orphan-branch"])
+ .output()
+ .unwrap();
+
+ // Setup a file to copy
+ fs::write(repo.root_path().join(".env"), "value").unwrap();
+ fs::write(repo.root_path().join(".gitignore"), ".env\n").unwrap();
+
+ // Try to copy to a branch with no worktree
+ assert_cmd_snapshot!(make_snapshot_cmd(
+ &repo,
+ "step",
+ &["copy-ignored", "--to", "orphan-branch"],
+ None,
+ ));
+}
diff --git a/tests/snapshots/integration__integration_tests__help__help_config_long.snap b/tests/snapshots/integration__integration_tests__help__help_config_long.snap
index 35bb18a7e..2a4576342 100644
--- a/tests/snapshots/integration__integration_tests__help__help_config_long.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_config_long.snap
@@ -7,7 +7,7 @@ info:
- "--help"
env:
CLICOLOR_FORCE: "1"
- COLUMNS: "150"
+ COLUMNS: "500"
GIT_EDITOR: ""
RUST_LOG: warn
SOURCE_DATE_EPOCH: "1735776000"
@@ -146,8 +146,7 @@ When project hooks run for the first time, Worktrunk prompts for approval. Appro
[2m[projects."my-project"]
[2mapproved-commands = ["npm ci", "npm test"]
-Manage approvals with [2mwt hook approvals add[0m to review and pre-approve commands, and [2mwt hook approvals clear[0m to reset (add [2m--global[0m to clear all
-projects).
+Manage approvals with [2mwt hook approvals add[0m to review and pre-approve commands, and [2mwt hook approvals clear[0m to reset (add [2m--global[0m to clear all projects).
[1mUser hooks
@@ -185,8 +184,7 @@ The [2m[list][0m section adds a URL column to [2mwt list[0m:
[2m[list]
[2murl = "http://localhost:{{ branch | hash_port }}"
-URLs are dimmed when the port isn't listening. The template supports [2m{{ branch }}[0m with filters [2mhash_port[0m (port 10000-19999) and [2msanitize[0m
-(filesystem-safe).
+URLs are dimmed when the port isn't listening. The template supports [2m{{ branch }}[0m with filters [2mhash_port[0m (port 10000-19999) and [2msanitize[0m (filesystem-safe).
[1mCI platform override
diff --git a/tests/snapshots/integration__integration_tests__help__help_hook_approvals.snap b/tests/snapshots/integration__integration_tests__help__help_hook_approvals.snap
index 4372e9186..a38c77109 100644
--- a/tests/snapshots/integration__integration_tests__help__help_hook_approvals.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_hook_approvals.snap
@@ -8,7 +8,7 @@ info:
- "--help"
env:
CLICOLOR_FORCE: "1"
- COLUMNS: "150"
+ COLUMNS: "500"
GIT_EDITOR: ""
RUST_LOG: warn
SOURCE_DATE_EPOCH: "1735776000"
@@ -58,5 +58,4 @@ Clear global approvals:
[32mHow approvals work
-Approved commands are saved to user config. Re-approval is required when the command template changes or the project moves. Use [2m--yes[0m to bypass
-prompts in CI.
+Approved commands are saved to user config. Re-approval is required when the command template changes or the project moves. Use [2m--yes[0m to bypass prompts in CI.
diff --git a/tests/snapshots/integration__integration_tests__help__help_list_long.snap b/tests/snapshots/integration__integration_tests__help__help_list_long.snap
index b275a949b..314fba747 100644
--- a/tests/snapshots/integration__integration_tests__help__help_list_long.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_list_long.snap
@@ -7,7 +7,7 @@ info:
- "--help"
env:
CLICOLOR_FORCE: "1"
- COLUMNS: "150"
+ COLUMNS: "500"
GIT_EDITOR: ""
RUST_LOG: warn
SOURCE_DATE_EPOCH: "1735776000"
@@ -63,8 +63,7 @@ Usage: [1m[36mwt list[0m [36m[OPTIONS]
Show all worktrees with their status. The table includes uncommitted changes, divergence from the default branch and remote, and optional CI status.
-The table renders progressively: branch names, paths, and commit hashes appear immediately, then status, divergence, and other columns fill in as
-background git operations complete. With [2m--full[0m, CI status fetches from the network — the table displays instantly and CI fills in as results arrive.
+The table renders progressively: branch names, paths, and commit hashes appear immediately, then status, divergence, and other columns fill in as background git operations complete. With [2m--full[0m, CI status fetches from the network — the table displays instantly and CI fills in as results arrive.
[32mExamples
@@ -117,9 +116,7 @@ The CI column shows GitHub/GitLab pipeline status:
⚠ yellow Fetch error (rate limit, network)
(blank) No upstream or no PR/MR
-CI indicators are clickable links to the PR or pipeline page. Any CI dot appears dimmed when there are unpushed local changes (stale status). PRs/MRs
-are checked first, then branch workflows/pipelines for branches with an upstream. Local-only branches show blank. Results are cached for 30-60
-seconds; use [2mwt config state[0m to view or clear.
+CI indicators are clickable links to the PR or pipeline page. Any CI dot appears dimmed when there are unpushed local changes (stale status). PRs/MRs are checked first, then branch workflows/pipelines for branches with an upstream. Local-only branches show blank. Results are cached for 30-60 seconds; use [2mwt config state[0m to view or clear.
[32mStatus symbols
diff --git a/tests/snapshots/integration__integration_tests__help__help_merge_long.snap b/tests/snapshots/integration__integration_tests__help__help_merge_long.snap
index e489c50ad..c337dfc95 100644
--- a/tests/snapshots/integration__integration_tests__help__help_merge_long.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_merge_long.snap
@@ -7,7 +7,7 @@ info:
- "--help"
env:
CLICOLOR_FORCE: "1"
- COLUMNS: "150"
+ COLUMNS: "500"
GIT_EDITOR: ""
RUST_LOG: warn
SOURCE_DATE_EPOCH: "1735776000"
@@ -100,30 +100,23 @@ Skip committing/squashing (rebase still runs unless --no-rebase):
[2mwt merge[0m runs these steps:
-1. [1mSquash[0m — Stages uncommitted changes, then combines all commits since target into one (like GitHub's "Squash and merge"). Use [2m--stage[0m to control
-what gets staged: [2mall[0m (default), [2mtracked[0m, or [2mnone[0m. A backup ref is saved to [2mrefs/wt-backup/[0m. With [2m--no-squash[0m, uncommitted changes become a
-separate commit and individual commits are preserved.
+1. [1mSquash[0m — Stages uncommitted changes, then combines all commits since target into one (like GitHub's "Squash and merge"). Use [2m--stage[0m to control what gets staged: [2mall[0m (default), [2mtracked[0m, or [2mnone[0m. A backup ref is saved to [2mrefs/wt-backup/[0m. With [2m--no-squash[0m, uncommitted changes become a separate commit and individual commits are preserved.
2. [1mRebase[0m — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately.
3. [1mPre-merge hooks[0m — Hooks run after rebase, before merge. Failures abort. See [2mwt hook[0m.
4. [1mMerge[0m — Fast-forward merge to the target branch. Non-fast-forward merges are rejected.
5. [1mPre-remove hooks[0m — Hooks run before removing worktree. Failures abort.
-6. [1mCleanup[0m — Removes the worktree and branch. Use [2m--no-remove[0m to keep the worktree. When already on the target branch or in the main worktree, the
-worktree is preserved.
+6. [1mCleanup[0m — Removes the worktree and branch. Use [2m--no-remove[0m to keep the worktree. When already on the target branch or in the main worktree, the worktree is preserved.
7. [1mPost-merge hooks[0m — Hooks run after cleanup. Failures are logged but don't abort.
-Use [2m--no-commit[0m to skip committing uncommitted changes and squashing; rebase still runs by default and can rewrite commits unless [2m--no-rebase[0m is
-passed. Useful after preparing commits manually with [2mwt step[0m. Requires a clean working tree.
+Use [2m--no-commit[0m to skip committing uncommitted changes and squashing; rebase still runs by default and can rewrite commits unless [2m--no-rebase[0m is passed. Useful after preparing commits manually with [2mwt step[0m. Requires a clean working tree.
[32mLocal CI
-For personal projects, pre-merge hooks open up the possibility of a workflow with much faster iteration — an order of magnitude more small changes
-instead of fewer large ones.
+For personal projects, pre-merge hooks open up the possibility of a workflow with much faster iteration — an order of magnitude more small changes instead of fewer large ones.
-Historically, ensuring tests ran before merging was difficult to enforce locally. Remote CI was valuable for the process as much as the checks: it
-guaranteed validation happened. [2mwt merge[0m brings that guarantee local.
+Historically, ensuring tests ran before merging was difficult to enforce locally. Remote CI was valuable for the process as much as the checks: it guaranteed validation happened. [2mwt merge[0m brings that guarantee local.
-The full workflow: start an agent (one of many) on a task, work elsewhere, return when it's ready. Review the diff, run [2mwt merge[0m, move on. Pre-merge
-hooks validate before merging — if they pass, the branch goes to the default branch and the worktree cleans up.
+The full workflow: start an agent (one of many) on a task, work elsewhere, return when it's ready. Review the diff, run [2mwt merge[0m, move on. Pre-merge hooks validate before merging — if they pass, the branch goes to the default branch and the worktree cleans up.
[2m[pre-merge]
[2mtest = "cargo test"
diff --git a/tests/snapshots/integration__integration_tests__help__help_remove_long.snap b/tests/snapshots/integration__integration_tests__help__help_remove_long.snap
index 515cf41c7..20006cd78 100644
--- a/tests/snapshots/integration__integration_tests__help__help_remove_long.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_remove_long.snap
@@ -7,7 +7,7 @@ info:
- "--help"
env:
CLICOLOR_FORCE: "1"
- COLUMNS: "150"
+ COLUMNS: "500"
GIT_EDITOR: ""
RUST_LOG: warn
SOURCE_DATE_EPOCH: "1735776000"
@@ -85,8 +85,7 @@ Force-delete an unmerged branch:
[32mBranch cleanup
-By default, branches are deleted when merging them would add nothing. This works with squash-merge and rebase workflows where commit history differs
-but file changes match.
+By default, branches are deleted when merging them would add nothing. This works with squash-merge and rebase workflows where commit history differs but file changes match.
Worktrunk checks five conditions (in order of cost):
@@ -96,8 +95,7 @@ Worktrunk checks five conditions (in order of cost):
4. [1mTrees match[0m — Branch tree SHA equals target tree SHA. Shows [2m⊂[0m.
5. [1mMerge adds nothing[0m — Simulated merge produces the same tree as target. Handles squash-merged branches where target has advanced. Shows [2m⊂[0m.
-The "same commit" check uses the local default branch; for other checks, [1mtarget[0m means the default branch, or its upstream (e.g., [2morigin/main[0m) when
-strictly ahead.
+The "same commit" check uses the local default branch; for other checks, [1mtarget[0m means the default branch, or its upstream (e.g., [2morigin/main[0m) when strictly ahead.
Branches showing [2m_[0m or [2m⊂[0m are dimmed as safe to delete.
@@ -105,8 +103,7 @@ Use [2m-D[0m to force-delete branches with unmerged changes. Use [2m--no-dele
[32mBackground removal
-Removal runs in the background by default (returns immediately). Logs are written to [2m.git/wt-logs/{branch}-remove.log[0m. Use [2m--foreground[0m to run in the
-foreground.
+Removal runs in the background by default (returns immediately). Logs are written to [2m.git/wt-logs/{branch}-remove.log[0m. Use [2m--foreground[0m to run in the foreground.
[32mShortcuts
diff --git a/tests/snapshots/integration__integration_tests__help__help_step_long.snap b/tests/snapshots/integration__integration_tests__help__help_step_long.snap
index f1dd0f6ba..fff269d36 100644
--- a/tests/snapshots/integration__integration_tests__help__help_step_long.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_step_long.snap
@@ -6,9 +6,11 @@ info:
- step
- "--help"
env:
+ CARGO_LLVM_COV: "1"
CLICOLOR_FORCE: "1"
COLUMNS: "150"
GIT_EDITOR: ""
+ LLVM_PROFILE_FILE: /Users/maximilian/workspace/worktrunk.worktreeinclude/target/llvm-cov-target/worktrunk.worktreeinclude-%p-%m.profraw
RUST_LOG: warn
SOURCE_DATE_EPOCH: "1735776000"
TERM: alacritty
@@ -29,7 +31,7 @@ Usage: [1m[36mwt step[0m [36m[OPTIONS][0m [36m
[1m[36msquash[0m Squash commits since branching
[1m[36mpush[0m Fast-forward target to current branch
[1m[36mrebase[0m Rebase onto target
- [1m[36mcopy-ignored[0m Copy [1m.worktreeinclude[0m files to another worktree
+ [1m[36mcopy-ignored[0m Copy gitignored files to another worktree
[1m[36mfor-each[0m [experimental] Run command in each worktree
[1m[32mOptions:
@@ -68,7 +70,7 @@ Manual merge workflow with review between steps:
- [2msquash[0m — Squash all branch commits into one with LLM-generated message
- [2mrebase[0m — Rebase onto target branch
- [2mpush[0m — Fast-forward target to current branch
-- [2mcopy-ignored[0m — Copy files listed in [2m.worktreeinclude
+- [2mcopy-ignored[0m — Copy gitignored files between worktrees
- [2mfor-each[0m — [experimental] Run a command in every worktree
[32mOptions
diff --git a/tests/snapshots/integration__integration_tests__help__help_step_short.snap b/tests/snapshots/integration__integration_tests__help__help_step_short.snap
index d04bbf000..44f3242ac 100644
--- a/tests/snapshots/integration__integration_tests__help__help_step_short.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_step_short.snap
@@ -6,9 +6,11 @@ info:
- step
- "-h"
env:
+ CARGO_LLVM_COV: "1"
CLICOLOR_FORCE: "1"
COLUMNS: "150"
GIT_EDITOR: ""
+ LLVM_PROFILE_FILE: /Users/maximilian/workspace/worktrunk.worktreeinclude/target/llvm-cov-target/worktrunk.worktreeinclude-%p-%m.profraw
RUST_LOG: warn
SOURCE_DATE_EPOCH: "1735776000"
TERM: alacritty
@@ -29,7 +31,7 @@ Usage: [1m[36mwt step[0m [36m[OPTIONS][0m [36m
[1m[36msquash[0m Squash commits since branching
[1m[36mpush[0m Fast-forward target to current branch
[1m[36mrebase[0m Rebase onto target
- [1m[36mcopy-ignored[0m Copy [1m.worktreeinclude[0m files to another worktree
+ [1m[36mcopy-ignored[0m Copy gitignored files to another worktree
[1m[36mfor-each[0m [experimental] Run command in each worktree
[1m[32mOptions:
diff --git a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap
index 5d82241c4..1e934c20e 100644
--- a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap
@@ -7,7 +7,7 @@ info:
- "--help"
env:
CLICOLOR_FORCE: "1"
- COLUMNS: "150"
+ COLUMNS: "500"
GIT_EDITOR: ""
RUST_LOG: warn
SOURCE_DATE_EPOCH: "1735776000"
@@ -47,16 +47,14 @@ Usage: [1m[36mwt switch[0m [36m[OPTIONS][0m [36m[0m [1m[36m[--
[1m[36m-x[0m, [1m[36m--execute[0m[36m [0m[36m
Command to run after switch
- Replaces the wt process with the command after switching, giving it full terminal control. Useful for launching editors, AI agents, or other
- interactive tools.
+ Replaces the wt process with the command after switching, giving it full terminal control. Useful for launching editors, AI agents, or other interactive tools.
Especially useful with shell aliases:
[1m[1malias wsc='wt switch --create -x claude'
[1mwsc feature-branch -- 'Fix GH #322'
- Then [1mwsc feature-branch[0m creates the worktree and launches Claude Code. Arguments after [1m--[0m are passed to the command, so [1mwsc feature -- 'Fix
- [1mGH #322'[0m runs [1mclaude 'Fix GH #322'[0m, starting Claude with a prompt.
+ Then [1mwsc feature-branch[0m creates the worktree and launches Claude Code. Arguments after [1m--[0m are passed to the command, so [1mwsc feature -- 'Fix GH #322'[0m runs [1mclaude 'Fix GH #322'[0m, starting Claude with a prompt.
[1m[36m-y[0m, [1m[36m--yes
Skip approval prompts
@@ -82,8 +80,7 @@ Usage: [1m[36mwt switch[0m [36m[OPTIONS][0m [36m[0m [1m[36m[--
Change directory to a worktree, creating one if needed.
-Worktrees are addressed by branch name; paths are computed from a configurable template. Unlike [2mgit switch[0m, this navigates between worktrees rather
-than changing branches in place.
+Worktrees are addressed by branch name; paths are computed from a configurable template. Unlike [2mgit switch[0m, this navigates between worktrees rather than changing branches in place.
[32mExamples
diff --git a/tests/snapshots/integration__integration_tests__list__list_long_commit_message.snap b/tests/snapshots/integration__integration_tests__list__list_long_commit_message.snap
index de689ed9c..f772bb3a0 100644
--- a/tests/snapshots/integration__integration_tests__list__list_long_commit_message.snap
+++ b/tests/snapshots/integration__integration_tests__list__list_long_commit_message.snap
@@ -7,7 +7,7 @@ info:
env:
APPDATA: "[TEST_CONFIG_HOME]"
CLICOLOR_FORCE: "1"
- COLUMNS: "150"
+ COLUMNS: "500"
GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]"
@@ -20,6 +20,7 @@ info:
PATH: "[PATH]"
RUST_LOG: warn
SOURCE_DATE_EPOCH: "1735776000"
+ TERM: alacritty
USERPROFILE: "[TEST_HOME]"
WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
@@ -30,7 +31,7 @@ exit_code: 0
----- stdout -----
[1mBranch[0m [1mStatus[0m [1mHEAD±[0m [1mmain↕[0m [1mPath[0m [1mRemote⇅[0m [1mCommit[0m [1mAge[0m [1mMessage
@ main [2m^[22m . [2mc80da958[0m [2m1d[0m [2mShort message
-+ [2mfeature-a[0m [2m⊂[22m [2m[31m↓1[0m [2m../repo.feature-a[0m [2m0fcd1a46[0m [2m1d[0m [2mThis is a very long commit message that should test how the me…
++ [2mfeature-a[0m [2m⊂[22m [2m[31m↓1[0m [2m../repo.feature-a[0m [2m0fcd1a46[0m [2m1d[0m [2mThis is a very long commit message that should test how the message column handles truncation and w…
[2m○[22m [2mShowing 2 worktrees
diff --git a/tests/snapshots/integration__integration_tests__list__list_shows_warning_on_git_error.snap b/tests/snapshots/integration__integration_tests__list__list_shows_warning_on_git_error.snap
index 3dc54109d..a70690a28 100644
--- a/tests/snapshots/integration__integration_tests__list__list_shows_warning_on_git_error.snap
+++ b/tests/snapshots/integration__integration_tests__list__list_shows_warning_on_git_error.snap
@@ -7,7 +7,7 @@ info:
env:
APPDATA: "[TEST_CONFIG_HOME]"
CLICOLOR_FORCE: "1"
- COLUMNS: "150"
+ COLUMNS: "500"
GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]"
@@ -39,8 +39,7 @@ exit_code: 0
[33m▲[39m [33mSome git operations failed:
[107m [0m [1mfeature[22m: commit-details (fatal: bad object 0000000000000000000000000000000000000000)
[107m [0m [1mfeature[22m: ahead-behind (fatal: Invalid symmetric difference expression main...0000000000000000000000000000000000000000)
-[107m [0m [1mfeature[22m: committed-trees-match (fatal: ambiguous argument '0000000000000000000000000000000000000000^{tree}': unknown revision or path not in the
-[107m [0m working tree.)
+[107m [0m [1mfeature[22m: committed-trees-match (fatal: ambiguous argument '0000000000000000000000000000000000000000^{tree}': unknown revision or path not in the working tree.)
[107m [0m [1mfeature[22m: has-file-changes (fatal: Invalid symmetric difference expression main...feature)
[107m [0m [1mfeature[22m: working-tree-diff (fatal: bad object HEAD)[39m
[2m↳[22m [2mTo create a diagnostic file, run with [90m-vv[39m[22m
diff --git a/tests/snapshots/integration__integration_tests__spacing_edge_cases__mixed_length_branch_names.snap b/tests/snapshots/integration__integration_tests__spacing_edge_cases__mixed_length_branch_names.snap
index b9eac8e0a..63065ea27 100644
--- a/tests/snapshots/integration__integration_tests__spacing_edge_cases__mixed_length_branch_names.snap
+++ b/tests/snapshots/integration__integration_tests__spacing_edge_cases__mixed_length_branch_names.snap
@@ -7,7 +7,7 @@ info:
env:
APPDATA: "[TEST_CONFIG_HOME]"
CLICOLOR_FORCE: "1"
- COLUMNS: "150"
+ COLUMNS: "500"
GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]"
@@ -20,6 +20,7 @@ info:
PATH: "[PATH]"
RUST_LOG: warn
SOURCE_DATE_EPOCH: "1735776000"
+ TERM: alacritty
USERPROFILE: "[TEST_HOME]"
WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
@@ -28,12 +29,12 @@ info:
success: true
exit_code: 0
----- stdout -----
- [1mBranch[0m [1mStatus[0m [1mHEAD±[0m [1mmain↕[0m [1mRemote⇅[0m [1mCommit[0m [1mAge[0m [1mMessage
-@ main [2m^[22m [2ma1e809f5[0m [2m1d[0m [2mInitial commit
-+ [2mextremely-long-branch-name-that-might-cause-layout-issues[0m [2m_[22m [2ma1e809f5[0m [2m1d[0m [2mInitial commit
-+ [2mmedium[0m [2m_[22m [2ma1e809f5[0m [2m1d[0m [2mInitial commit
-+ [2mx[0m [2m_[22m [2ma1e809f5[0m [2m1d[0m [2mInitial commit
+ [1mBranch[0m [1mStatus[0m [1mHEAD±[0m [1mmain↕[0m [1mPath[0m [1mRemote⇅[0m [1mCommit[0m [1mAge[0m [1mMessage
+@ main [2m^[22m . [2ma1e809f5[0m [2m1d[0m [2mInitial commit
++ [2mextremely-long-branch-name-that-might-cause-layout-issues[0m [2m_[22m [2m../repo.extremely-long-branch-name-that-might-cause-layout-issues[0m [2ma1e809f5[0m [2m1d[0m [2mInitial commit
++ [2mmedium[0m [2m_[22m [2m../repo.medium[0m [2ma1e809f5[0m [2m1d[0m [2mInitial commit
++ [2mx[0m [2m_[22m [2m../repo.x[0m [2ma1e809f5[0m [2m1d[0m [2mInitial commit
-[2m○[22m [2mShowing 4 worktrees, 1 column hidden
+[2m○[22m [2mShowing 4 worktrees
----- stderr -----
diff --git a/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_default_copies_all.snap b/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_default_copies_all.snap
new file mode 100644
index 000000000..9b022ed3a
--- /dev/null
+++ b/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_default_copies_all.snap
@@ -0,0 +1,35 @@
+---
+source: tests/integration_tests/step_copy_ignored.rs
+info:
+ program: wt
+ args:
+ - step
+ - copy-ignored
+ env:
+ APPDATA: "[TEST_CONFIG_HOME]"
+ CLICOLOR_FORCE: "1"
+ COLUMNS: "150"
+ GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
+ GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
+ GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]"
+ GIT_CONFIG_SYSTEM: /dev/null
+ GIT_EDITOR: ""
+ GIT_TERMINAL_PROMPT: "0"
+ HOME: "[TEST_HOME]"
+ LANG: C
+ LC_ALL: C
+ PATH: "[PATH]"
+ RUST_LOG: warn
+ SOURCE_DATE_EPOCH: "1735776000"
+ TERM: alacritty
+ USERPROFILE: "[TEST_HOME]"
+ WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
+ WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
+ XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
+---
+success: true
+exit_code: 0
+----- stdout -----
+
+----- stderr -----
+[32m✓[39m [32mCopied 2 entries[39m
diff --git a/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_from_nonexistent_worktree.snap b/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_from_nonexistent_worktree.snap
new file mode 100644
index 000000000..057f1eb26
--- /dev/null
+++ b/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_from_nonexistent_worktree.snap
@@ -0,0 +1,37 @@
+---
+source: tests/integration_tests/step_copy_ignored.rs
+info:
+ program: wt
+ args:
+ - step
+ - copy-ignored
+ - "--from"
+ - orphan-branch
+ env:
+ APPDATA: "[TEST_CONFIG_HOME]"
+ CLICOLOR_FORCE: "1"
+ COLUMNS: "150"
+ GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
+ GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
+ GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]"
+ GIT_CONFIG_SYSTEM: /dev/null
+ GIT_EDITOR: ""
+ GIT_TERMINAL_PROMPT: "0"
+ HOME: "[TEST_HOME]"
+ LANG: C
+ LC_ALL: C
+ PATH: "[PATH]"
+ RUST_LOG: warn
+ SOURCE_DATE_EPOCH: "1735776000"
+ TERM: alacritty
+ USERPROFILE: "[TEST_HOME]"
+ WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
+ WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
+ XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
+---
+success: false
+exit_code: 1
+----- stdout -----
+
+----- stderr -----
+[31m✗[39m [31mNo worktree found for branch [1morphan-branch[22m[39m
diff --git a/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_invalid_worktreeinclude.snap b/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_invalid_worktreeinclude.snap
new file mode 100644
index 000000000..439327247
--- /dev/null
+++ b/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_invalid_worktreeinclude.snap
@@ -0,0 +1,36 @@
+---
+source: tests/integration_tests/step_copy_ignored.rs
+info:
+ program: wt
+ args:
+ - step
+ - copy-ignored
+ env:
+ APPDATA: "[TEST_CONFIG_HOME]"
+ CLICOLOR_FORCE: "1"
+ COLUMNS: "150"
+ GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
+ GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
+ GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]"
+ GIT_CONFIG_SYSTEM: /dev/null
+ GIT_EDITOR: ""
+ GIT_TERMINAL_PROMPT: "0"
+ HOME: "[TEST_HOME]"
+ LANG: C
+ LC_ALL: C
+ PATH: "[PATH]"
+ RUST_LOG: warn
+ SOURCE_DATE_EPOCH: "1735776000"
+ TERM: alacritty
+ USERPROFILE: "[TEST_HOME]"
+ WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
+ WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
+ XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
+---
+success: false
+exit_code: 1
+----- stdout -----
+
+----- stderr -----
+[31m✗[39m [31mError parsing [1m.worktreeinclude[22m[39m
+[107m [0m _REPO_/.worktreeinclude: line 1: error parsing glob '{unclosed': unclosed alternate group; missing '}' (maybe escape '{' with '[{]'?)
diff --git a/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_no_worktreeinclude.snap b/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_no_worktreeinclude.snap
index 7053e034e..be466b678 100644
--- a/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_no_worktreeinclude.snap
+++ b/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_no_worktreeinclude.snap
@@ -32,4 +32,4 @@ exit_code: 0
----- stdout -----
----- stderr -----
-[2m○[22m No [1m.worktreeinclude[22m file found
+[2m○[22m No matching files to copy
diff --git a/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_to_nonexistent_worktree.snap b/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_to_nonexistent_worktree.snap
new file mode 100644
index 000000000..d28c2362f
--- /dev/null
+++ b/tests/snapshots/integration__integration_tests__step_copy_ignored__copy_ignored_to_nonexistent_worktree.snap
@@ -0,0 +1,37 @@
+---
+source: tests/integration_tests/step_copy_ignored.rs
+info:
+ program: wt
+ args:
+ - step
+ - copy-ignored
+ - "--to"
+ - orphan-branch
+ env:
+ APPDATA: "[TEST_CONFIG_HOME]"
+ CLICOLOR_FORCE: "1"
+ COLUMNS: "150"
+ GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
+ GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
+ GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]"
+ GIT_CONFIG_SYSTEM: /dev/null
+ GIT_EDITOR: ""
+ GIT_TERMINAL_PROMPT: "0"
+ HOME: "[TEST_HOME]"
+ LANG: C
+ LC_ALL: C
+ PATH: "[PATH]"
+ RUST_LOG: warn
+ SOURCE_DATE_EPOCH: "1735776000"
+ TERM: alacritty
+ USERPROFILE: "[TEST_HOME]"
+ WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
+ WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
+ XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
+---
+success: false
+exit_code: 1
+----- stdout -----
+
+----- stderr -----
+[31m✗[39m [31mNo worktree found for branch [1morphan-branch[22m[39m