Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8adb08b
feat: add system-wide config file support
claude Feb 10, 2026
5c27cd3
fix: apply cargo fmt formatting
goodtune Feb 10, 2026
ec5d240
fix: ensure consistent system config path in tests across platforms
goodtune Feb 10, 2026
76da863
style: apply cargo fmt to tests/common/mod.rs
claude Feb 10, 2026
97bcf99
fix: add WORKTRUNK_SYSTEM_CONFIG_PATH to PTY test environment
goodtune Feb 10, 2026
4707172
Merge branch 'main' into claude/system-config-paths-CVQDx
goodtune Feb 10, 2026
283fa89
Merge branch 'main' into claude/system-config-paths-CVQDx
max-sixty Feb 11, 2026
6e90df3
refactor: remove #[cfg(test)] guards from config path resolution
max-sixty Feb 11, 2026
d8471e1
test: add integration tests for XDG_CONFIG_DIRS and platform default …
max-sixty Feb 11, 2026
59e51db
test: add coverage for system config edge cases in config show
max-sixty Feb 11, 2026
4f02212
docs: reduce system config prominence in config page
max-sixty Feb 12, 2026
baeed27
Merge branch 'main' into claude/system-config-paths-CVQDx
max-sixty Feb 13, 2026
15f6d45
Merge branch 'main' into claude/system-config-paths-CVQDx
max-sixty Feb 13, 2026
64c3f80
Merge branch 'main' into claude/system-config-paths-CVQDx
max-sixty Feb 13, 2026
167d011
fix: update powershell test snapshot for system config section
max-sixty Feb 13, 2026
f643473
Merge branch 'main' into claude/system-config-paths-CVQDx
goodtune Feb 16, 2026
f4d7de8
Merge branch 'main' into claude/system-config-paths-CVQDx
goodtune Feb 16, 2026
a6f8bae
fix(config): resolve list timeout from per-project config (#1063)
max-sixty Feb 16, 2026
bbb9128
feat(switch): enrich error hints with --execute context (#1064)
max-sixty Feb 16, 2026
1118e6e
Refactor approvals into separate file with deprecation migration (#1042)
max-sixty Feb 16, 2026
c2c1b3e
refactor: consolidate shell escaping on shell_escape, drop shlex (#1065)
max-sixty Feb 16, 2026
34afcc8
fix(test): wait for item content in switch picker PTY tests (#1066)
worktrunk-bot Feb 16, 2026
b39430f
feat(switch): add AI summary preview tab (#1049)
max-sixty Feb 16, 2026
5a281b3
fix: replace missed shlex::try_quote call with shell_escape (#1067)
worktrunk-bot Feb 16, 2026
3164ec1
Improve CI reviewer: resolve threads, skip trivial re-approval, defau…
max-sixty Feb 16, 2026
5cab670
fix: update snapshots and PTY tests after merge from main
max-sixty Feb 17, 2026
77dd2dc
Merge branch 'main' into claude/system-config-paths-CVQDx
max-sixty Feb 17, 2026
d356334
Refactor system config handling to only show section when found
max-sixty Feb 17, 2026
8950d66
fix: improve review findings and add hooks merge semantics tests
max-sixty Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/content/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ wt config show
| **User config** | `~/.config/worktrunk/config.toml` | Worktree path template, LLM commit configs, etc | ✗ |
| **Project config** | `.config/wt.toml` | Project hooks, dev server URL | ✓ |

Organizations can also deploy a system-wide config file for shared defaults — run `wt config show` for the platform-specific location.

**User config** — personal preferences:

```toml
Expand Down Expand Up @@ -353,6 +355,8 @@ WORKTRUNK_COMMIT__GENERATION__COMMAND="echo 'test: automated commit'" wt merge
|----------|---------|
| `WORKTRUNK_BIN` | Override binary path for shell wrappers (useful for testing dev builds) |
| `WORKTRUNK_CONFIG_PATH` | Override user config file location |
| `WORKTRUNK_SYSTEM_CONFIG_PATH` | Override system config file location |
| `XDG_CONFIG_DIRS` | Colon-separated system config directories (default: `/etc/xdg`) |
| `WORKTRUNK_DIRECTIVE_FILE` | Internal: set by shell wrappers to enable directory changes |
| `WORKTRUNK_SHELL` | Internal: set by shell wrappers to indicate shell type (e.g., `powershell`) |
| `WORKTRUNK_MAX_CONCURRENT_COMMANDS` | Max parallel git commands (default: 32). Lower if hitting file descriptor limits. |
Expand Down Expand Up @@ -396,7 +400,7 @@ Usage: <b><span class=c>wt config</span></b> <span class=c>[OPTIONS]</span> <spa
Show configuration files & locations.

Shows location and contents of user config (`~/.config/worktrunk/config.toml`)
and project config (`.config/wt.toml`).
and project config (`.config/wt.toml`). Also shows system config if present.

If a config file doesn't exist, shows defaults that would be used.

Expand Down
6 changes: 5 additions & 1 deletion skills/worktrunk/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ wt config show
| **User config** | `~/.config/worktrunk/config.toml` | Worktree path template, LLM commit configs, etc | ✗ |
| **Project config** | `.config/wt.toml` | Project hooks, dev server URL | ✓ |

Organizations can also deploy a system-wide config file for shared defaults — run `wt config show` for the platform-specific location.

**User config** — personal preferences:

```toml
Expand Down Expand Up @@ -345,6 +347,8 @@ WORKTRUNK_COMMIT__GENERATION__COMMAND="echo 'test: automated commit'" wt merge
|----------|---------|
| `WORKTRUNK_BIN` | Override binary path for shell wrappers (useful for testing dev builds) |
| `WORKTRUNK_CONFIG_PATH` | Override user config file location |
| `WORKTRUNK_SYSTEM_CONFIG_PATH` | Override system config file location |
| `XDG_CONFIG_DIRS` | Colon-separated system config directories (default: `/etc/xdg`) |
| `WORKTRUNK_DIRECTIVE_FILE` | Internal: set by shell wrappers to enable directory changes |
| `WORKTRUNK_SHELL` | Internal: set by shell wrappers to indicate shell type (e.g., `powershell`) |
| `WORKTRUNK_MAX_CONCURRENT_COMMANDS` | Max parallel git commands (default: 32). Lower if hitting file descriptor limits. |
Expand Down Expand Up @@ -386,7 +390,7 @@ Usage: <b><span class=c>wt config</span></b> <span class=c>[OPTIONS]</span> <spa
Show configuration files & locations.

Shows location and contents of user config (`~/.config/worktrunk/config.toml`)
and project config (`.config/wt.toml`).
and project config (`.config/wt.toml`). Also shows system config if present.

If a config file doesn't exist, shows defaults that would be used.

Expand Down
2 changes: 1 addition & 1 deletion src/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ pub enum ConfigCommand {
/// Show configuration files & locations
#[command(
after_long_help = r#"Shows location and contents of user config (`~/.config/worktrunk/config.toml`)
and project config (`.config/wt.toml`).
and project config (`.config/wt.toml`). Also shows system config if present.

If a config file doesn't exist, shows defaults that would be used.

Expand Down
4 changes: 4 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,8 @@ wt config show
| **User config** | `~/.config/worktrunk/config.toml` | Worktree path template, LLM commit configs, etc | ✗ |
| **Project config** | `.config/wt.toml` | Project hooks, dev server URL | ✓ |

Organizations can also deploy a system-wide config file for shared defaults — run `wt config show` for the platform-specific location.

**User config** — personal preferences:

```toml
Expand Down Expand Up @@ -1797,6 +1799,8 @@ WORKTRUNK_COMMIT__GENERATION__COMMAND="echo 'test: automated commit'" wt merge
|----------|---------|
| `WORKTRUNK_BIN` | Override binary path for shell wrappers (useful for testing dev builds) |
| `WORKTRUNK_CONFIG_PATH` | Override user config file location |
| `WORKTRUNK_SYSTEM_CONFIG_PATH` | Override system config file location |
| `XDG_CONFIG_DIRS` | Colon-separated system config directories (default: `/etc/xdg`) |
| `WORKTRUNK_DIRECTIVE_FILE` | Internal: set by shell wrappers to enable directory changes |
| `WORKTRUNK_SHELL` | Internal: set by shell wrappers to indicate shell type (e.g., `powershell`) |
| `WORKTRUNK_MAX_CONCURRENT_COMMANDS` | Max parallel git commands (default: 32). Lower if hitting file descriptor limits. |
Expand Down
72 changes: 69 additions & 3 deletions src/commands/config/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use std::path::PathBuf;
use anyhow::Context;
use color_print::cformat;
use worktrunk::config::{
ProjectConfig, UserConfig, find_unknown_project_keys, find_unknown_user_keys,
ProjectConfig, UserConfig, default_system_config_path, find_unknown_project_keys,
find_unknown_user_keys, get_system_config_path,
};
use worktrunk::git::Repository;
use worktrunk::path::format_path_for_display;
Expand All @@ -34,8 +35,14 @@ pub fn handle_config_show(full: bool) -> anyhow::Result<()> {
// Build the complete output as a string
let mut show_output = String::new();

// Render system config section (only when a system config file exists)
let has_system_config = render_system_config(&mut show_output)?;
if has_system_config {
show_output.push('\n');
}

// Render user config
render_user_config(&mut show_output)?;
render_user_config(&mut show_output, has_system_config)?;
show_output.push('\n');

// Render project config if in a git repository
Expand Down Expand Up @@ -340,7 +347,48 @@ fn render_diagnostics(out: &mut String) -> anyhow::Result<()> {
Ok(())
}

fn render_user_config(out: &mut String) -> anyhow::Result<()> {
/// Render the SYSTEM CONFIG section. Returns true if a system config file was found.
fn render_system_config(out: &mut String) -> anyhow::Result<bool> {
let Some(system_path) = get_system_config_path() else {
return Ok(false);
};

writeln!(
out,
"{}",
format_heading(
"SYSTEM CONFIG",
Some(&format_path_for_display(&system_path))
)
)?;

// Read and display the file contents
let contents =
std::fs::read_to_string(&system_path).context("Failed to read system config file")?;

if contents.trim().is_empty() {
writeln!(out, "{}", hint_message("Empty file (no system defaults)"))?;
return Ok(true);
}

// Validate config (syntax + schema) and warn if invalid
if let Err(e) = toml::from_str::<UserConfig>(&contents) {
writeln!(out, "{}", error_message("Invalid config"))?;
writeln!(out, "{}", format_with_gutter(&e.to_string(), None))?;
} else {
// Only check for unknown keys if config is valid
out.push_str(&warn_unknown_keys::<UserConfig>(&find_unknown_user_keys(
&contents,
)));
}

// Display TOML with syntax highlighting
writeln!(out, "{}", format_toml(&contents))?;

Ok(true)
}

fn render_user_config(out: &mut String, has_system_config: bool) -> anyhow::Result<()> {
let config_path = require_user_config_path()?;

writeln!(
Expand Down Expand Up @@ -406,6 +454,24 @@ fn render_user_config(out: &mut String) -> anyhow::Result<()> {
// Display TOML with syntax highlighting (gutter at column 0)
writeln!(out, "{}", format_toml(&contents))?;

if !has_system_config {
render_system_config_hint(out)?;
}

Ok(())
}

fn render_system_config_hint(out: &mut String) -> anyhow::Result<()> {
if let Some(path) = default_system_config_path() {
writeln!(
out,
"{}",
hint_message(cformat!(
"Optional system config not found @ <dim>{}</>",
format_path_for_display(&path)
))
)?;
}
Ok(())
}

Expand Down
21 changes: 10 additions & 11 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
//! Configuration system for worktrunk
//!
//! Worktrunk uses two independent configuration files:
//! Three configuration sources, loaded in order (later overrides earlier):
//!
//! - **User config** (`~/.config/worktrunk/config.toml`) - Personal preferences
//! - **Project config** (`.config/wt.toml`) - Lifecycle hooks, checked into git
//! 1. **System config** (`/etc/xdg/worktrunk/config.toml` or platform equivalent) -
//! Organization-wide defaults, optional
//! 2. **User config** (`~/.config/worktrunk/config.toml`) - Personal preferences
//! 3. **Project config** (`.config/wt.toml`) - Lifecycle hooks, checked into git
//!
//! The two configs are **completely independent**:
//! - No overlap in settings (they configure different things)
//! - No merging or precedence rules needed
//! - Loaded separately and used in different contexts
//!
//! User config controls "how worktrunk behaves for me", project config controls
//! "what commands run for this project".
//! System and user configs share the same schema and are merged by the `config`
//! crate's builder (user values override system values at the key level).
//! Project config is independent — different schema, different purpose.
//!
//! See `wt config --help` for complete documentation.

Expand Down Expand Up @@ -96,7 +94,8 @@ pub use project::{
pub use user::{
CommitConfig, CommitGenerationConfig, ListConfig, MergeConfig, OverridableConfig,
ResolvedConfig, SelectConfig, StageMode, UserConfig, UserProjectOverrides,
find_unknown_keys as find_unknown_user_keys, get_config_path, set_config_path,
default_system_config_path, find_unknown_keys as find_unknown_user_keys, get_config_path,
get_system_config_path, set_config_path,
};

#[cfg(test)]
Expand Down
32 changes: 27 additions & 5 deletions src/config/user/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ use serde::{Deserialize, Serialize};

// Re-export public types
pub use merge::Merge;
pub use path::{get_config_path, set_config_path};
pub use path::{
default_system_config_path, get_config_path, get_system_config_path, set_config_path,
};
pub use resolved::ResolvedConfig;
pub use schema::{find_unknown_keys, valid_user_config_keys};
pub use sections::{
Expand Down Expand Up @@ -106,19 +108,39 @@ pub struct UserConfig {
}

impl UserConfig {
/// Load configuration from config file and environment variables.
/// Load configuration from system config, user config, and environment variables.
///
/// Configuration is loaded in the following order (later sources override earlier ones):
/// 1. Default values
/// 2. Config file (see struct documentation for platform-specific paths)
/// 3. Environment variables (WORKTRUNK_*)
/// 2. System config (organization-wide defaults)
/// 3. User config file (personal preferences)
/// 4. Environment variables (WORKTRUNK_*)
pub fn load() -> Result<Self, ConfigError> {
// Note: worktree-path has no default set here - it's handled by the getter
// which returns the default when None. This allows us to distinguish
// "user explicitly set this" from "using default".
let mut builder = Config::builder();

// Add config file if it exists
// Add system config if it exists (lowest priority file source)
if let Some(system_path) = path::get_system_config_path() {
if let Ok(content) = std::fs::read_to_string(&system_path) {
// Warn about unknown fields in system config
let unknown_keys: std::collections::HashMap<_, _> = find_unknown_keys(&content)
.into_iter()
.filter(|(k, _)| {
!super::deprecation::DEPRECATED_SECTION_KEYS.contains(&k.as_str())
})
.collect();
super::deprecation::warn_unknown_fields::<UserConfig>(
&system_path,
&unknown_keys,
"System config",
);
}
builder = builder.add_source(File::from(system_path));
}

// Add user config file if it exists (overrides system config)
let config_path = get_config_path();
if let Some(config_path) = config_path.as_ref()
&& config_path.exists()
Expand Down
Loading
Loading