diff --git a/.gitignore b/.gitignore
index d0b9f14d10..2c348dec72 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/target
+.DS_Store
docs/static/assets/
docs/demos/out/
diff --git a/Cargo.lock b/Cargo.lock
index 8b706d6cd3..cd1292eb6c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3199,6 +3199,7 @@ dependencies = [
"env_logger",
"etcetera",
"fs2",
+ "glob",
"home",
"humantime",
"ignore",
diff --git a/Cargo.toml b/Cargo.toml
index 005628eb88..bcb06ab70e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -126,6 +126,7 @@ schemars = { version = "1.2.1", features = ["derive"] }
tempfile = "3.27"
wait-timeout = "0.2"
+glob = "0.3.3"
[target.'cfg(unix)'.dependencies]
skim = "0.20"
diff --git a/dev/config.example.toml b/dev/config.example.toml
index f616901682..5c051cc9a6 100644
--- a/dev/config.example.toml
+++ b/dev/config.example.toml
@@ -85,6 +85,7 @@
#
# task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables
# timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
+# hidden = ["tmp-*"] # [experimental] Glob patterns to hide from output
#
# ### Commit
#
diff --git a/docs/content/config.md b/docs/content/config.md
index 32490a6d1e..59d4d7f6f8 100644
--- a/docs/content/config.md
+++ b/docs/content/config.md
@@ -166,6 +166,7 @@ remotes = false # Include remote-only branches (--remotes)
task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables
timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
+hidden = ["tmp-*"] # Glob patterns to hide from output
```
### Commit
diff --git a/docs/content/list.md b/docs/content/list.md
index 17c5944282..da971c46e5 100644
--- a/docs/content/list.md
+++ b/docs/content/list.md
@@ -132,6 +132,21 @@ These appear across all columns while the table is loading:
---
+## Filtering with hidden patterns
+
+
+
+The `[list].hidden` config hides worktrees and branches from output. Patterns are matched against both the canonical worktree path and the branch name:
+
+```toml
+[list]
+hidden = ["tmp-*", "*/scratch/*"]
+```
+
+A worktree or branch is hidden if any pattern matches its path or branch name. Filtering applies to worktrees, local branches (`--branches`), and remote branches (`--remotes`). The summary line shows how many items were filtered (e.g. `Showing 3 worktrees, 1 hidden`).
+
+---
+
## JSON output
Query structured data with `--format=json`:
diff --git a/skills/worktrunk/reference/config.md b/skills/worktrunk/reference/config.md
index 293d2a45b1..b1d1476760 100644
--- a/skills/worktrunk/reference/config.md
+++ b/skills/worktrunk/reference/config.md
@@ -165,6 +165,7 @@ remotes = false # Include remote-only branches (--remotes)
task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables
timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
+hidden = ["tmp-*"] # [experimental] Glob patterns to hide from output
```
### Commit
diff --git a/skills/worktrunk/reference/list.md b/skills/worktrunk/reference/list.md
index d6e1cc2281..29ab2d7965 100644
--- a/skills/worktrunk/reference/list.md
+++ b/skills/worktrunk/reference/list.md
@@ -122,6 +122,19 @@ These appear across all columns while the table is loading:
---
+## Filtering with hidden patterns [experimental]
+
+The `[list].hidden` config hides worktrees and branches from output. Patterns are matched against both the canonical worktree path and the branch name:
+
+```toml
+[list]
+hidden = ["tmp-*", "*/scratch/*"]
+```
+
+A worktree or branch is hidden if any pattern matches its path or branch name. Filtering applies to worktrees, local branches (`--branches`), and remote branches (`--remotes`). The summary line shows how many items were filtered (e.g. `Showing 3 worktrees, 1 hidden`).
+
+---
+
## JSON output
Query structured data with `--format=json`:
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 2ab37e6c65..efce9f5751 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -390,6 +390,7 @@ pub(crate) struct ListArgs {
#[arg(long)]
pub(crate) full: bool,
+ // TODO: consider adding a flag to show hidden items (name TBD β `--hidden`, `--all`, etc.)
/// Show fast info immediately, update with slow info
///
/// Displays local data (branches, paths, status) first, then updates
@@ -771,6 +772,19 @@ These appear across all columns while the table is loading:
---
+## Filtering with hidden patterns [experimental]
+
+The `[list].hidden` config hides worktrees and branches from output. Patterns are matched against both the canonical worktree path and the branch name:
+
+```toml
+[list]
+hidden = ["tmp-*", "*/scratch/*"]
+```
+
+A worktree or branch is hidden if any pattern matches its path or branch name. Filtering applies to worktrees, local branches (`--branches`), and remote branches (`--remotes`). The summary line shows how many items were filtered (e.g. `Showing 3 worktrees, 1 hidden`).
+
+---
+
## JSON output
Query structured data with `--format=json`:
@@ -1790,6 +1804,7 @@ remotes = false # Include remote-only branches (--remotes)
task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables
timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
+hidden = ["tmp-*"] # [experimental] Glob patterns to hide from output
```
### Commit
diff --git a/src/commands/list/collect/mod.rs b/src/commands/list/collect/mod.rs
index a8f0f11725..7fb9108e11 100644
--- a/src/commands/list/collect/mod.rs
+++ b/src/commands/list/collect/mod.rs
@@ -361,59 +361,108 @@ pub fn collect(
let url_template = url_template_cell.into_inner().unwrap();
// Resolve show flags: merge CLI overrides with config (warmed in parallel phase)
- let (show_branches, show_remotes, skip_tasks, command_timeout, collect_deadline) =
- match show_config {
- ShowConfig::Resolved {
- show_branches,
- show_remotes,
- skip_tasks,
- command_timeout,
- collect_deadline,
- } => (
+ let (
+ show_branches,
+ show_remotes,
+ skip_tasks,
+ command_timeout,
+ collect_deadline,
+ hidden_patterns,
+ ) = match show_config {
+ ShowConfig::Resolved {
+ show_branches,
+ show_remotes,
+ skip_tasks,
+ command_timeout,
+ collect_deadline,
+ } => (
+ show_branches,
+ show_remotes,
+ skip_tasks,
+ command_timeout,
+ collect_deadline,
+ None,
+ ),
+ ShowConfig::DeferredToParallel {
+ cli_branches,
+ cli_remotes,
+ cli_full,
+ } => {
+ let config = repo.config();
+ let show_branches = cli_branches || config.list.branches();
+ let show_remotes = cli_remotes || config.list.remotes();
+ let show_full = cli_full || config.list.full();
+ let skip_tasks: HashSet = if show_full {
+ HashSet::new()
+ } else {
+ [
+ TaskKind::BranchDiff,
+ TaskKind::CiStatus,
+ TaskKind::WorkingTreeConflicts,
+ TaskKind::SummaryGenerate,
+ ]
+ .into_iter()
+ .collect()
+ };
+ // Resolve timeouts from merged config (--full disables both)
+ let (command_timeout, collect_deadline) = if show_full {
+ (None, None)
+ } else {
+ let task_timeout = config.list.task_timeout();
+ let deadline = config.list.timeout().map(|d| std::time::Instant::now() + d);
+ (task_timeout, deadline)
+ };
+ let hidden_patterns = config.list.hidden.clone();
+ (
show_branches,
show_remotes,
skip_tasks,
command_timeout,
collect_deadline,
- ),
- ShowConfig::DeferredToParallel {
- cli_branches,
- cli_remotes,
- cli_full,
- } => {
- let config = repo.config();
- let show_branches = cli_branches || config.list.branches();
- let show_remotes = cli_remotes || config.list.remotes();
- let show_full = cli_full || config.list.full();
- let skip_tasks: HashSet = if show_full {
- HashSet::new()
- } else {
- [
- TaskKind::BranchDiff,
- TaskKind::CiStatus,
- TaskKind::WorkingTreeConflicts,
- TaskKind::SummaryGenerate,
- ]
- .into_iter()
- .collect()
- };
- // Resolve timeouts from merged config (--full disables both)
- let (command_timeout, collect_deadline) = if show_full {
- (None, None)
- } else {
- let task_timeout = config.list.task_timeout();
- let deadline = config.list.timeout().map(|d| std::time::Instant::now() + d);
- (task_timeout, deadline)
- };
- (
- show_branches,
- show_remotes,
- skip_tasks,
- command_timeout,
- collect_deadline,
- )
- }
- };
+ hidden_patterns,
+ )
+ }
+ };
+
+ // Compile hidden patterns once for use across worktrees, branches, and remotes.
+ // Uses string matching (not path matching) for branch names since they contain
+ // `/` separators but aren't filesystem paths.
+ let compiled_hidden: Vec = hidden_patterns
+ .as_ref()
+ .map(|p| {
+ p.iter()
+ .filter_map(|s| match glob::Pattern::new(s) {
+ Ok(pat) => Some(pat),
+ Err(e) => {
+ log::warn!("Invalid [list].hidden pattern {:?}: {}", s, e);
+ None
+ }
+ })
+ .collect()
+ })
+ .unwrap_or_default();
+ let branch_hidden =
+ |name: &str| -> bool { compiled_hidden.iter().any(|pat| pat.matches(name)) };
+
+ // Filter out hidden worktrees by path or branch name
+ let (worktrees, hidden_worktree_count) = if compiled_hidden.is_empty() {
+ (worktrees, 0)
+ } else {
+ let before = worktrees.len();
+ let filtered: Vec<_> = worktrees
+ .into_iter()
+ .filter(|wt| {
+ let path = canonicalize(&wt.path)
+ .ok()
+ .unwrap_or_else(|| wt.path.clone());
+ let path_matches = compiled_hidden.iter().any(|pat| pat.matches_path(&path));
+ let name_matches = wt.branch.as_deref().is_some_and(&branch_hidden);
+ !(path_matches || name_matches)
+ })
+ .collect();
+ let hidden = before - filtered.len();
+ (filtered, hidden)
+ };
// Filter local branches to those without worktrees (CPU-only, no git commands)
let branches_without_worktrees = if show_branches {
@@ -431,6 +480,20 @@ pub fn collect(
} else {
Vec::new()
};
+
+ // Filter hidden local branches
+ let (branches_without_worktrees, hidden_branch_count) = if compiled_hidden.is_empty() {
+ (branches_without_worktrees, 0)
+ } else {
+ let before = branches_without_worktrees.len();
+ let filtered: Vec<_> = branches_without_worktrees
+ .into_iter()
+ .filter(|(name, _)| !branch_hidden(name))
+ .collect();
+ let hidden = before - filtered.len();
+ (filtered, hidden)
+ };
+
let remote_branches = if show_remotes {
if let Some(result) = remote_branches_cell.into_inner() {
result?
@@ -442,6 +505,21 @@ pub fn collect(
Vec::new()
};
+ // Filter hidden remote branches
+ let (remote_branches, hidden_remote_count) = if compiled_hidden.is_empty() {
+ (remote_branches, 0)
+ } else {
+ let before = remote_branches.len();
+ let filtered: Vec<_> = remote_branches
+ .into_iter()
+ .filter(|(name, _)| !branch_hidden(name))
+ .collect();
+ let hidden = before - filtered.len();
+ (filtered, hidden)
+ };
+
+ let total_hidden = hidden_worktree_count + hidden_branch_count + hidden_remote_count;
+
// Detect current worktree using git rev-parse --show-toplevel (via WorkingTree::root).
// This correctly handles worktrees placed inside other worktrees (e.g., .worktrees/ layout)
// by letting git resolve the actual worktree root rather than using prefix matching.
@@ -985,6 +1063,7 @@ pub fn collect(
&all_items,
show_branches || show_remotes,
layout.hidden_column_count,
+ total_hidden,
error_count,
timed_out_count,
),
diff --git a/src/commands/list/mod.rs b/src/commands/list/mod.rs
index 076652693d..0c37b5aec8 100644
--- a/src/commands/list/mod.rs
+++ b/src/commands/list/mod.rs
@@ -206,6 +206,7 @@ pub(super) struct SummaryMetrics {
worktrees: usize,
local_branches: usize,
remote_branches: usize,
+ hidden: usize,
dirty_worktrees: usize,
ahead_items: usize,
}
@@ -266,6 +267,10 @@ impl SummaryMetrics {
parts.push(format!("{} worktree{}", self.worktrees, plural));
}
+ if self.hidden > 0 {
+ parts.push(format!("{} hidden", self.hidden));
+ }
+
if self.dirty_worktrees > 0 {
parts.push(format!("{} with changes", self.dirty_worktrees));
}
@@ -292,10 +297,12 @@ pub(crate) fn format_summary_message(
items: &[ListItem],
show_branches: bool,
hidden_column_count: usize,
+ hidden_count: usize,
error_count: usize,
timed_out_count: usize,
) -> String {
- let metrics = SummaryMetrics::from_items(items);
+ let mut metrics = SummaryMetrics::from_items(items);
+ metrics.hidden = hidden_count;
let dim = Style::new().dimmed();
let summary = metrics
.summary_parts(show_branches, hidden_column_count)
@@ -331,6 +338,7 @@ mod tests {
assert_eq!(metrics.worktrees, 0);
assert_eq!(metrics.local_branches, 0);
assert_eq!(metrics.remote_branches, 0);
+ assert_eq!(metrics.hidden, 0);
assert_eq!(metrics.dirty_worktrees, 0);
assert_eq!(metrics.ahead_items, 0);
}
@@ -341,6 +349,7 @@ mod tests {
worktrees: 1,
local_branches: 0,
remote_branches: 0,
+ hidden: 0,
dirty_worktrees: 0,
ahead_items: 0,
};
@@ -354,6 +363,7 @@ mod tests {
worktrees: 3,
local_branches: 0,
remote_branches: 0,
+ hidden: 0,
dirty_worktrees: 0,
ahead_items: 0,
};
@@ -367,6 +377,7 @@ mod tests {
worktrees: 2,
local_branches: 5,
remote_branches: 10,
+ hidden: 0,
dirty_worktrees: 0,
ahead_items: 0,
};
@@ -383,6 +394,7 @@ mod tests {
worktrees: 3,
local_branches: 0,
remote_branches: 0,
+ hidden: 0,
dirty_worktrees: 2,
ahead_items: 0,
};
@@ -396,6 +408,7 @@ mod tests {
worktrees: 2,
local_branches: 0,
remote_branches: 0,
+ hidden: 0,
dirty_worktrees: 0,
ahead_items: 1,
};
@@ -409,6 +422,7 @@ mod tests {
worktrees: 1,
local_branches: 0,
remote_branches: 0,
+ hidden: 0,
dirty_worktrees: 0,
ahead_items: 0,
};
@@ -425,6 +439,7 @@ mod tests {
worktrees: 2,
local_branches: 0,
remote_branches: 5,
+ hidden: 0,
dirty_worktrees: 0,
ahead_items: 0,
};
@@ -438,6 +453,7 @@ mod tests {
worktrees: 5,
local_branches: 3,
remote_branches: 8,
+ hidden: 0,
dirty_worktrees: 2,
ahead_items: 4,
};
@@ -460,16 +476,18 @@ mod tests {
use insta::assert_snapshot;
// No errors
- assert_snapshot!(format_summary_message(&[], false, 0, 0, 0), @"[2mβ[22m [2mShowing 0 worktrees[0m");
+ assert_snapshot!(format_summary_message(&[], false, 0, 0, 0, 0), @"[2mβ[22m [2mShowing 0 worktrees[0m");
// All timeouts
- assert_snapshot!(format_summary_message(&[], false, 0, 3, 3), @"[2mβ[22m [2mShowing 0 worktrees. 3 tasks timed out[0m");
+ assert_snapshot!(format_summary_message(&[], false, 0, 0, 3, 3), @"[2mβ[22m [2mShowing 0 worktrees. 3 tasks timed out[0m");
// Mixed errors and timeouts
- assert_snapshot!(format_summary_message(&[], false, 0, 5, 3), @"[2mβ[22m [2mShowing 0 worktrees. 5 tasks failed (3 timed out)[0m");
+ assert_snapshot!(format_summary_message(&[], false, 0, 0, 5, 3), @"[2mβ[22m [2mShowing 0 worktrees. 5 tasks failed (3 timed out)[0m");
// Only failures, no timeouts
- assert_snapshot!(format_summary_message(&[], false, 0, 2, 0), @"[2mβ[22m [2mShowing 0 worktrees. 2 tasks failed[0m");
+ assert_snapshot!(format_summary_message(&[], false, 0, 0, 2, 0), @"[2mβ[22m [2mShowing 0 worktrees. 2 tasks failed[0m");
// Single error
- assert_snapshot!(format_summary_message(&[], false, 0, 1, 0), @"[2mβ[22m [2mShowing 0 worktrees. 1 task failed[0m");
+ assert_snapshot!(format_summary_message(&[], false, 0, 0, 1, 0), @"[2mβ[22m [2mShowing 0 worktrees. 1 task failed[0m");
+ // With hidden items
+ assert_snapshot!(format_summary_message(&[], false, 0, 3, 0, 0), @"[2mβ[22m [2mShowing 0 worktrees, 3 hidden[0m");
// Single timeout
- assert_snapshot!(format_summary_message(&[], false, 0, 1, 1), @"[2mβ[22m [2mShowing 0 worktrees. 1 task timed out[0m");
+ assert_snapshot!(format_summary_message(&[], false, 0, 0, 1, 1), @"[2mβ[22m [2mShowing 0 worktrees. 1 task timed out[0m");
}
}
diff --git a/src/config/user/sections.rs b/src/config/user/sections.rs
index 8abe076304..f8d7e85b11 100644
--- a/src/config/user/sections.rs
+++ b/src/config/user/sections.rs
@@ -149,6 +149,11 @@ pub struct ListConfig {
/// Disabled when --full is used. Default: no budget (wait for all results).
#[serde(rename = "timeout-ms", skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option,
+
+ /// Glob patterns for items to hide from list output.
+ /// Matches against worktree paths and branch names.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub hidden: Option>,
}
impl ListConfig {
@@ -198,6 +203,7 @@ impl Merge for ListConfig {
summary: other.summary.or(self.summary),
task_timeout_ms: other.task_timeout_ms.or(self.task_timeout_ms),
timeout_ms: other.timeout_ms.or(self.timeout_ms),
+ hidden: other.hidden.clone().or_else(|| self.hidden.clone()),
}
}
}
diff --git a/src/config/user/tests.rs b/src/config/user/tests.rs
index 34c78cf2a1..5360e48f1a 100644
--- a/src/config/user/tests.rs
+++ b/src/config/user/tests.rs
@@ -292,6 +292,7 @@ fn test_list_config_serde() {
summary: None,
task_timeout_ms: Some(500),
timeout_ms: None,
+ hidden: None,
};
let json = serde_json::to_string(&config).unwrap();
let parsed: ListConfig = serde_json::from_str(&json).unwrap();
@@ -533,6 +534,7 @@ fn test_merge_list_config() {
summary: Some(true),
task_timeout_ms: Some(1000),
timeout_ms: Some(2000),
+ hidden: None,
};
let override_config = ListConfig {
full: None, // Should fall back to base
@@ -541,6 +543,7 @@ fn test_merge_list_config() {
summary: None, // Should fall back to base
task_timeout_ms: None, // Should fall back to base
timeout_ms: None, // Should fall back to base
+ hidden: None,
};
let merged = base.merge_with(&override_config);
@@ -947,6 +950,7 @@ fn test_list_config_accessor_methods_with_values() {
summary: Some(true),
task_timeout_ms: Some(5000),
timeout_ms: Some(3000),
+ hidden: None,
};
assert!(config.full());
assert!(config.branches());
diff --git a/tests/integration_tests/list_config.rs b/tests/integration_tests/list_config.rs
index 14130a2c4e..bd9bce98f9 100644
--- a/tests/integration_tests/list_config.rs
+++ b/tests/integration_tests/list_config.rs
@@ -1,7 +1,8 @@
//! Tests for `wt list` command with user config
use crate::common::{
- TestRepo, repo, set_temp_home_env, setup_snapshot_settings_with_home, temp_home, wt_command,
+ TestRepo, list_snapshots, repo, set_temp_home_env, setup_snapshot_settings_with_home,
+ temp_home, wt_command,
};
use insta_cmd::assert_cmd_snapshot;
use rstest::rstest;
@@ -473,3 +474,124 @@ task-timeout-ms = 1
stderr
);
}
+
+#[rstest]
+fn test_list_config_hidden_single_glob(mut repo: TestRepo) {
+ // Create worktrees: one matching the hidden pattern, one not
+ repo.add_worktree("feature");
+ repo.add_worktree("tmp-scratch");
+
+ // Write config with hidden pattern to the test config path
+ // Pattern matches worktree directory names containing "tmp-"
+ repo.write_test_config(
+ r#"worktree-path = "../{{ repo }}.{{ branch }}"
+
+[list]
+hidden = ["*/repo.tmp-*"]
+"#,
+ );
+
+ assert_cmd_snapshot!(list_snapshots::command(&repo, repo.root_path()));
+}
+
+#[rstest]
+fn test_list_config_hidden_array_globs(mut repo: TestRepo) {
+ // Create worktrees matching different patterns
+ repo.add_worktree("feature");
+ repo.add_worktree("tmp-one");
+ repo.add_worktree("scratch-two");
+
+ // Write config with multiple hidden patterns
+ // Patterns match worktree directory names containing "tmp-" or "scratch-"
+ repo.write_test_config(
+ r#"worktree-path = "../{{ repo }}.{{ branch }}"
+
+[list]
+hidden = ["*/repo.tmp-*", "*/repo.scratch-*"]
+"#,
+ );
+
+ assert_cmd_snapshot!(list_snapshots::command(&repo, repo.root_path()));
+}
+
+#[rstest]
+fn test_list_config_hidden_by_parent_path(mut repo: TestRepo) {
+ // Create worktrees in different parent directories
+ let temp_dir = repo.root_path().parent().unwrap();
+ let scratch_dir = temp_dir.join("scratch");
+ let keep_dir = temp_dir.join("keep");
+
+ // Worktree in "scratch" folder (should be ignored)
+ repo.add_worktree_at_path("feature-scratch", &scratch_dir.join("repo.feature-scratch"));
+
+ // Worktree in "keep" folder (should be kept)
+ repo.add_worktree_at_path("feature-keep", &keep_dir.join("repo.feature-keep"));
+
+ // Configure hidden pattern matching the parent directory
+ repo.write_test_config(
+ r#"worktree-path = "{{ branch }}"
+
+[list]
+hidden = ["*/scratch/*"]
+"#,
+ );
+
+ assert_cmd_snapshot!(list_snapshots::command(&repo, repo.root_path()));
+}
+
+#[rstest]
+fn test_list_config_hidden_worktree_by_branch_name(mut repo: TestRepo) {
+ repo.add_worktree("feature");
+ repo.add_worktree("tmp-scratch");
+
+ // Pattern matches branch name directly (no path glob needed)
+ repo.write_test_config(
+ r#"worktree-path = "../{{ repo }}.{{ branch }}"
+
+[list]
+hidden = ["tmp-*"]
+"#,
+ );
+
+ assert_cmd_snapshot!(list_snapshots::command(&repo, repo.root_path()));
+}
+
+#[rstest]
+fn test_list_config_hidden_local_branch(repo: TestRepo) {
+ // Remove fixture worktrees to isolate test (keep only main worktree)
+ for branch in &["feature-a", "feature-b", "feature-c"] {
+ let worktree_path = repo
+ .root_path()
+ .parent()
+ .unwrap()
+ .join(format!("repo.{}", branch));
+ if worktree_path.exists() {
+ let _ = repo
+ .git_command()
+ .args([
+ "worktree",
+ "remove",
+ "--force",
+ worktree_path.to_str().unwrap(),
+ ])
+ .run();
+ }
+ let _ = repo.git_command().args(["branch", "-D", branch]).run();
+ }
+
+ // Create branches without worktrees
+ repo.run_git(&["branch", "feature"]);
+ repo.run_git(&["branch", "tmp-experiment"]);
+
+ repo.write_test_config(
+ r#"worktree-path = "../{{ repo }}.{{ branch }}"
+
+[list]
+hidden = ["tmp-*"]
+"#,
+ );
+
+ let mut cmd = list_snapshots::command(&repo, repo.root_path());
+ cmd.arg("--branches");
+ assert_cmd_snapshot!(cmd);
+}
diff --git a/tests/snapshots/integration__integration_tests__help__help_config_create.snap b/tests/snapshots/integration__integration_tests__help__help_config_create.snap
index 080399b74c..af29cfb470 100644
--- a/tests/snapshots/integration__integration_tests__help__help_config_create.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_config_create.snap
@@ -145,6 +145,7 @@ Creates [2m~/.config/worktrunk/config.toml[0m with the following content:
[107m [0m [2m#[0m
[107m [0m [2m# task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables[0m
[107m [0m [2m# timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables[0m
+[107m [0m [2m# hidden = ["tmp-*"] # [experimental] Glob patterns to hide from output[0m
[107m [0m [2m#[0m
[107m [0m [2m# ### Commit[0m
[107m [0m [2m#[0m
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 ca747d898a..43f55cbc11 100644
--- a/tests/snapshots/integration__integration_tests__help__help_config_long.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_config_long.snap
@@ -191,6 +191,7 @@ Persistent flag values for [2mwt list[0m. Override on command line as needed.
[107m [0m
[107m [0m [2mtask-timeout-ms = [0m[2m[33m0[0m[2m [0m[2m# Kill individual git commands after N ms; 0 disables[0m
[107m [0m [2mtimeout-ms = [0m[2m[33m0[0m[2m [0m[2m# Wall-clock budget for the entire collect phase; 0 disables[0m
+[107m [0m [2mhidden = [[0m[2m[32m"tmp-*"[0m[2m] [0m[2m# [experimental] Glob patterns to hide from output[0m
[32mCommit[0m
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 70d2b74e5e..320203d5de 100644
--- a/tests/snapshots/integration__integration_tests__help__help_list_long.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_list_long.snap
@@ -182,6 +182,17 @@ These appear across all columns while the table is loading:
[2mββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ[0m
+[1m[32mFiltering with hidden patterns [experimental][0m
+
+The [2m[list].hidden[0m config hides worktrees and branches from output. Patterns are matched against both the canonical worktree path and the branch name:
+
+[107m [0m [2m[36m[list][0m
+[107m [0m [2mhidden = [[0m[2m[32m"tmp-*"[0m[2m, [0m[2m[32m"*/scratch/*"[0m[2m][0m
+
+A worktree or branch is hidden if any pattern matches its path or branch name. Filtering applies to worktrees, local branches ([2m--branches[0m), and remote branches ([2m--remotes[0m). The summary line shows how many items were filtered (e.g. [2mShowing 3 worktrees, 1 hidden[0m).
+
+[2mββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ[0m
+
[1m[32mJSON output[0m
Query structured data with [2m--format=json[0m:
diff --git a/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap b/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap
index 18f5ce1654..940a022e5d 100644
--- a/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap
+++ b/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap
@@ -204,6 +204,21 @@ These appear across all columns while the table is loading:
[2mββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ[0m
+[1m[32mFiltering with hidden patterns [experimental][0m
+
+The [2m[list].hidden[0m config hides worktrees and branches from output. Patterns are
+matched against both the canonical worktree path and the branch name:
+
+[107m [0m [2m[36m[list][0m
+[107m [0m [2mhidden = [[0m[2m[32m"tmp-*"[0m[2m, [0m[2m[32m"*/scratch/*"[0m[2m][0m
+
+A worktree or branch is hidden if any pattern matches its path or branch name.
+Filtering applies to worktrees, local branches ([2m--branches[0m), and remote branches
+ ([2m--remotes[0m). The summary line shows how many items were filtered (e.g. [2mShowing
+[2m3 worktrees, 1 hidden[0m).
+
+[2mββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ[0m
+
[1m[32mJSON output[0m
Query structured data with [2m--format=json[0m:
diff --git a/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_array_globs.snap b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_array_globs.snap
new file mode 100644
index 0000000000..919454d996
--- /dev/null
+++ b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_array_globs.snap
@@ -0,0 +1,51 @@
+---
+source: tests/integration_tests/list_config.rs
+info:
+ program: wt
+ args:
+ - list
+ env:
+ APPDATA: "[TEST_CONFIG_HOME]"
+ CLICOLOR_FORCE: "1"
+ COLUMNS: "500"
+ 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
+ MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]"
+ NO_COLOR: ""
+ PATH: "[PATH]"
+ PSModulePath: ""
+ RUST_LOG: warn
+ SHELL: ""
+ TERM: alacritty
+ USERPROFILE: "[TEST_HOME]"
+ WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]"
+ WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
+ WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]"
+ WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
+ WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1"
+ WORKTRUNK_TEST_EPOCH: "1735776000"
+ WORKTRUNK_TEST_NUSHELL_ENV: "0"
+ WORKTRUNK_TEST_POWERSHELL_ENV: "0"
+ WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
+ XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
+---
+success: true
+exit_code: 0
+----- stdout -----
+ [1mBranch[0m [1mStatus[0m [1mHEADΒ±[0m [1mmainβ[0m [1mRemoteβ
[0m [1mPath[0m [1mCommit[0m [1mAge[0m [1mMessage
+@ main [2m^[22m[2m|[22m [2m|[0m . [2m05a4a45d[0m [2m16h[0m [2mInitial commit
++ [2mfeature[0m [2m_[22m [2m../repo.feature[0m [2m05a4a45d[0m [2m16h[0m [2mInitial commit
++ feature-a [2mβ[22m [32mβ1[0m ../repo.feature-a [2m1b87d473[0m [2m16h[0m [2mAdd feature-a file
++ feature-b [2mβ[22m [32mβ1[0m ../repo.feature-b [2mf62940fc[0m [2m16h[0m [2mAdd feature-b file
++ feature-c [2mβ[22m [32mβ1[0m ../repo.feature-c [2m345c7c93[0m [2m16h[0m [2mAdd feature-c file
+
+[2mβ[22m [2mShowing 5 worktrees, 2 hidden, 3 ahead
+
+----- stderr -----
diff --git a/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_by_parent_path.snap b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_by_parent_path.snap
new file mode 100644
index 0000000000..9efdaac5e1
--- /dev/null
+++ b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_by_parent_path.snap
@@ -0,0 +1,51 @@
+---
+source: tests/integration_tests/list_config.rs
+info:
+ program: wt
+ args:
+ - list
+ env:
+ APPDATA: "[TEST_CONFIG_HOME]"
+ CLICOLOR_FORCE: "1"
+ COLUMNS: "500"
+ 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
+ MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]"
+ NO_COLOR: ""
+ PATH: "[PATH]"
+ PSModulePath: ""
+ RUST_LOG: warn
+ SHELL: ""
+ TERM: alacritty
+ USERPROFILE: "[TEST_HOME]"
+ WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]"
+ WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
+ WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]"
+ WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
+ WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1"
+ WORKTRUNK_TEST_EPOCH: "1735776000"
+ WORKTRUNK_TEST_NUSHELL_ENV: "0"
+ WORKTRUNK_TEST_POWERSHELL_ENV: "0"
+ WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
+ XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
+---
+success: true
+exit_code: 0
+----- stdout -----
+ [1mBranch[0m [1mStatus[0m [1mHEADΒ±[0m [1mmainβ[0m [1mRemoteβ
[0m [1mPath[0m [1mCommit[0m [1mAge[0m [1mMessage
+@ main [2m^[22m[2m|[22m [2m|[0m . [2m05a4a45d[0m [2m16h[0m [2mInitial commit
++ [2mfeature-keep[0m [31mβ[39m[2m_[22m [2m../keep/repo.feature-keep[0m [2m05a4a45d[0m [2m16h[0m [2mInitial commit
++ feature-a [31mβ[39m[2mβ[22m [32mβ1[0m ../repo.feature-a [2m1b87d473[0m [2m16h[0m [2mAdd feature-a file
++ feature-b [31mβ[39m[2mβ[22m [32mβ1[0m ../repo.feature-b [2mf62940fc[0m [2m16h[0m [2mAdd feature-b file
++ feature-c [31mβ[39m[2mβ[22m [32mβ1[0m ../repo.feature-c [2m345c7c93[0m [2m16h[0m [2mAdd feature-c file
+
+[2mβ[22m [2mShowing 5 worktrees, 1 hidden, 3 ahead
+
+----- stderr -----
diff --git a/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_local_branch.snap b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_local_branch.snap
new file mode 100644
index 0000000000..2c768dead0
--- /dev/null
+++ b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_local_branch.snap
@@ -0,0 +1,49 @@
+---
+source: tests/integration_tests/list_config.rs
+info:
+ program: wt
+ args:
+ - list
+ - "--branches"
+ env:
+ APPDATA: "[TEST_CONFIG_HOME]"
+ CLICOLOR_FORCE: "1"
+ COLUMNS: "500"
+ 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
+ MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]"
+ NO_COLOR: ""
+ PATH: "[PATH]"
+ PSModulePath: ""
+ RUST_LOG: warn
+ SHELL: ""
+ TERM: alacritty
+ USERPROFILE: "[TEST_HOME]"
+ WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]"
+ WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
+ WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]"
+ WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
+ WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1"
+ WORKTRUNK_TEST_EPOCH: "1735776000"
+ WORKTRUNK_TEST_NUSHELL_ENV: "0"
+ WORKTRUNK_TEST_POWERSHELL_ENV: "0"
+ WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
+ XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
+---
+success: true
+exit_code: 0
+----- stdout -----
+ [1mBranch[0m [1mStatus[0m [1mHEADΒ±[0m [1mmainβ[0m [1mRemoteβ
[0m [1mPath[0m [1mCommit[0m [1mAge[0m [1mMessage
+@ main [2m^[22m[2m|[22m [2m|[0m . [2m05a4a45d[0m [2m16h[0m [2mInitial commit
+ [2mfeature[0m [2m/[22m[2m_[22m [2m05a4a45d[0m [2m16h[0m [2mInitial commit
+
+[2mβ[22m [2mShowing 1 worktrees, 1 branches, 1 hidden
+
+----- stderr -----
diff --git a/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_single_glob.snap b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_single_glob.snap
new file mode 100644
index 0000000000..b55c07641c
--- /dev/null
+++ b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_single_glob.snap
@@ -0,0 +1,51 @@
+---
+source: tests/integration_tests/list_config.rs
+info:
+ program: wt
+ args:
+ - list
+ env:
+ APPDATA: "[TEST_CONFIG_HOME]"
+ CLICOLOR_FORCE: "1"
+ COLUMNS: "500"
+ 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
+ MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]"
+ NO_COLOR: ""
+ PATH: "[PATH]"
+ PSModulePath: ""
+ RUST_LOG: warn
+ SHELL: ""
+ TERM: alacritty
+ USERPROFILE: "[TEST_HOME]"
+ WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]"
+ WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
+ WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]"
+ WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
+ WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1"
+ WORKTRUNK_TEST_EPOCH: "1735776000"
+ WORKTRUNK_TEST_NUSHELL_ENV: "0"
+ WORKTRUNK_TEST_POWERSHELL_ENV: "0"
+ WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
+ XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
+---
+success: true
+exit_code: 0
+----- stdout -----
+ [1mBranch[0m [1mStatus[0m [1mHEADΒ±[0m [1mmainβ[0m [1mRemoteβ
[0m [1mPath[0m [1mCommit[0m [1mAge[0m [1mMessage
+@ main [2m^[22m[2m|[22m [2m|[0m . [2m05a4a45d[0m [2m16h[0m [2mInitial commit
++ [2mfeature[0m [2m_[22m [2m../repo.feature[0m [2m05a4a45d[0m [2m16h[0m [2mInitial commit
++ feature-a [2mβ[22m [32mβ1[0m ../repo.feature-a [2m1b87d473[0m [2m16h[0m [2mAdd feature-a file
++ feature-b [2mβ[22m [32mβ1[0m ../repo.feature-b [2mf62940fc[0m [2m16h[0m [2mAdd feature-b file
++ feature-c [2mβ[22m [32mβ1[0m ../repo.feature-c [2m345c7c93[0m [2m16h[0m [2mAdd feature-c file
+
+[2mβ[22m [2mShowing 5 worktrees, 1 hidden, 3 ahead
+
+----- stderr -----
diff --git a/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_worktree_by_branch_name.snap b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_worktree_by_branch_name.snap
new file mode 100644
index 0000000000..b55c07641c
--- /dev/null
+++ b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_worktree_by_branch_name.snap
@@ -0,0 +1,51 @@
+---
+source: tests/integration_tests/list_config.rs
+info:
+ program: wt
+ args:
+ - list
+ env:
+ APPDATA: "[TEST_CONFIG_HOME]"
+ CLICOLOR_FORCE: "1"
+ COLUMNS: "500"
+ 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
+ MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]"
+ NO_COLOR: ""
+ PATH: "[PATH]"
+ PSModulePath: ""
+ RUST_LOG: warn
+ SHELL: ""
+ TERM: alacritty
+ USERPROFILE: "[TEST_HOME]"
+ WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]"
+ WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
+ WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]"
+ WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
+ WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1"
+ WORKTRUNK_TEST_EPOCH: "1735776000"
+ WORKTRUNK_TEST_NUSHELL_ENV: "0"
+ WORKTRUNK_TEST_POWERSHELL_ENV: "0"
+ WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
+ XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
+---
+success: true
+exit_code: 0
+----- stdout -----
+ [1mBranch[0m [1mStatus[0m [1mHEADΒ±[0m [1mmainβ[0m [1mRemoteβ
[0m [1mPath[0m [1mCommit[0m [1mAge[0m [1mMessage
+@ main [2m^[22m[2m|[22m [2m|[0m . [2m05a4a45d[0m [2m16h[0m [2mInitial commit
++ [2mfeature[0m [2m_[22m [2m../repo.feature[0m [2m05a4a45d[0m [2m16h[0m [2mInitial commit
++ feature-a [2mβ[22m [32mβ1[0m ../repo.feature-a [2m1b87d473[0m [2m16h[0m [2mAdd feature-a file
++ feature-b [2mβ[22m [32mβ1[0m ../repo.feature-b [2mf62940fc[0m [2m16h[0m [2mAdd feature-b file
++ feature-c [2mβ[22m [32mβ1[0m ../repo.feature-c [2m345c7c93[0m [2m16h[0m [2mAdd feature-c file
+
+[2mβ[22m [2mShowing 5 worktrees, 1 hidden, 3 ahead
+
+----- stderr -----