Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions docs/content/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,28 @@ url = "echo http://localhost:{{ branch | hash_port }}"
Aliases defined here are shared with teammates. For personal aliases, use the [user config](@/config.md#aliases) `[aliases]` section instead.
<!-- PROJECT_CONFIG_END -->

## Private project config in git config

<span class="badge-experimental"></span>

Project config normally lives in `.config/wt.toml`, committed and shared. Some settings are better kept private: a hook that runs a personal script, a machine-specific dev-server URL. Git config can hold these.

Any key under the `worktrunk.config.` prefix in git config becomes project config. Strip the prefix; what remains is the exact TOML key path from the sections above:

{{ terminal(cmd="git config worktrunk.config.post-start 'pnpm install'|||git config worktrunk.config.list.url 'http://localhost:3000'") }}

`.git/config` is local to the repository and never committed, so these keys stay on one machine — and every linked worktree sees them, because the local scope lives in the shared git dir. `--global` puts a key in every repository. Git's normal precedence applies: local overrides global, and conditional includes work.

Selection is all-or-nothing. When any `worktrunk.config.*` key exists, those keys are the complete project config and `.config/wt.toml` is ignored — a warning names the superseded file. There is no key-level merging between the two sources. To return to the file, remove the keys.

Values are strings, one per key. Settings that need other TOML types (such as the `step.copy-ignored.exclude` array) cannot be expressed here. Hooks and aliases defined this way go through the same approval prompt as file-based project config.

Use the canonical key spellings from the sections above. Deprecated spellings may still deserialize, but git-sourced configuration does not run file migration or emit deprecation guidance — `wt config update` has nothing to rewrite here. Per-worktree git config (`extensions.worktreeConfig`) is not supported: keys are read from the shared git dir, so a `config.worktree` value is never consumed. Setting `WORKTRUNK_PROJECT_CONFIG_PATH` — even to an empty value — disables this source entirely; the override names the project config source outright.

To list the matching git keys with their scope and origin file (inside a linked worktree this can also show worktree-scoped keys, which worktrunk does not read):

{{ terminal(cmd="git config --show-scope --show-origin --get-regexp '^worktrunk\.config\.'") }}

# Shell Integration

Worktrunk needs shell integration to change directories when switching worktrees. Install with:
Expand Down
25 changes: 25 additions & 0 deletions plugins/worktrunk/skills/worktrunk/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,31 @@ url = "echo http://localhost:{{ branch | hash_port }}"
Aliases defined here are shared with teammates. For personal aliases, use the [user config](https://worktrunk.dev/config/#aliases) `[aliases]` section instead.
<!-- PROJECT_CONFIG_END -->

## Private project config in git config [experimental]

Project config normally lives in `.config/wt.toml`, committed and shared. Some settings are better kept private: a hook that runs a personal script, a machine-specific dev-server URL. Git config can hold these.

Any key under the `worktrunk.config.` prefix in git config becomes project config. Strip the prefix; what remains is the exact TOML key path from the sections above:

```bash
$ git config worktrunk.config.post-start 'pnpm install'
$ git config worktrunk.config.list.url 'http://localhost:3000'
```

`.git/config` is local to the repository and never committed, so these keys stay on one machine — and every linked worktree sees them, because the local scope lives in the shared git dir. `--global` puts a key in every repository. Git's normal precedence applies: local overrides global, and conditional includes work.

Selection is all-or-nothing. When any `worktrunk.config.*` key exists, those keys are the complete project config and `.config/wt.toml` is ignored — a warning names the superseded file. There is no key-level merging between the two sources. To return to the file, remove the keys.

Values are strings, one per key. Settings that need other TOML types (such as the `step.copy-ignored.exclude` array) cannot be expressed here. Hooks and aliases defined this way go through the same approval prompt as file-based project config.

Use the canonical key spellings from the sections above. Deprecated spellings may still deserialize, but git-sourced configuration does not run file migration or emit deprecation guidance — `wt config update` has nothing to rewrite here. Per-worktree git config (`extensions.worktreeConfig`) is not supported: keys are read from the shared git dir, so a `config.worktree` value is never consumed. Setting `WORKTRUNK_PROJECT_CONFIG_PATH` — even to an empty value — disables this source entirely; the override names the project config source outright.

To list the matching git keys with their scope and origin file (inside a linked worktree this can also show worktree-scoped keys, which worktrunk does not read):

```bash
$ git config --show-scope --show-origin --get-regexp '^worktrunk\.config\.'
```

# Shell Integration

Worktrunk needs shell integration to change directories when switching worktrees. Install with:
Expand Down
25 changes: 25 additions & 0 deletions skills/worktrunk/reference/config.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2495,6 +2495,31 @@ url = "echo http://localhost:{{ branch | hash_port }}"
Aliases defined here are shared with teammates. For personal aliases, use the [user config](@/config.md#aliases) `[aliases]` section instead.
<!-- PROJECT_CONFIG_END -->

## Private project config in git config [experimental]

Project config normally lives in `.config/wt.toml`, committed and shared. Some settings are better kept private: a hook that runs a personal script, a machine-specific dev-server URL. Git config can hold these.

Any key under the `worktrunk.config.` prefix in git config becomes project config. Strip the prefix; what remains is the exact TOML key path from the sections above:

```console
$ git config worktrunk.config.post-start 'pnpm install'
$ git config worktrunk.config.list.url 'http://localhost:3000'
```

`.git/config` is local to the repository and never committed, so these keys stay on one machine — and every linked worktree sees them, because the local scope lives in the shared git dir. `--global` puts a key in every repository. Git's normal precedence applies: local overrides global, and conditional includes work.

Selection is all-or-nothing. When any `worktrunk.config.*` key exists, those keys are the complete project config and `.config/wt.toml` is ignored — a warning names the superseded file. There is no key-level merging between the two sources. To return to the file, remove the keys.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two facts are stated separately but never multiplied: --global reaches every repository, and selection is all-or-nothing. Together they mean one global key set for convenience in one repo silently disables the committed project config — and every project hook — in all of them. The load-time warning catches it, but only after the fact.

Suggested change
Selection is all-or-nothing. When any `worktrunk.config.*` key exists, those keys are the complete project config and `.config/wt.toml` is ignored — a warning names the superseded file. There is no key-level merging between the two sources. To return to the file, remove the keys.
Selection is all-or-nothing. When any `worktrunk.config.*` key exists, those keys are the complete project config and `.config/wt.toml` is ignored — a warning names the superseded file. There is no key-level merging between the two sources. To return to the file, remove the keys. Combined with `--global` this reaches every repository at once: a single global key supersedes the committed project config of every repository on the machine, so keep keys repository-local (or scope them with `includeIf`) unless that is the intent.

The mirrors under docs/content/ and skills/worktrunk/reference/ regenerate from this.


Values are strings, one per key. Settings that need other TOML types (such as the `step.copy-ignored.exclude` array) cannot be expressed here. Hooks and aliases defined this way go through the same approval prompt as file-based project config.

Use the canonical key spellings from the sections above. Deprecated spellings may still deserialize, but git-sourced configuration does not run file migration or emit deprecation guidance — `wt config update` has nothing to rewrite here. Per-worktree git config (`extensions.worktreeConfig`) is not supported: keys are read from the shared git dir, so a `config.worktree` value is never consumed. Setting `WORKTRUNK_PROJECT_CONFIG_PATH` — even to an empty value — disables this source entirely; the override names the project config source outright.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two corrections here.

a config.worktree value is never consumed holds only for a linked worktree. The main worktree's worktree-scoped file is $GIT_COMMON_DIR/config.worktree, so the common-dir read that all_config() performs does include it — prewarm_git_config_from_common_dir's docstring depends on that exact property. A key placed there under extensions.worktreeConfig therefore supplies project config repository-wide.

Second, git lowercases the final key component, so a user-chosen name is silently renamed: git config worktrunk.config.aliases.Deploy 'make deploy' lists as worktrunk.config.aliases.deploy and is invoked as wt deploy. "Use the canonical key spellings from the sections above" doesn't cover it, since alias names aren't schema-spelled.

Suggested change
Use the canonical key spellings from the sections above. Deprecated spellings may still deserialize, but git-sourced configuration does not run file migration or emit deprecation guidance — `wt config update` has nothing to rewrite here. Per-worktree git config (`extensions.worktreeConfig`) is not supported: keys are read from the shared git dir, so a `config.worktree` value is never consumed. Setting `WORKTRUNK_PROJECT_CONFIG_PATH` — even to an empty value — disables this source entirely; the override names the project config source outright.
Use the canonical key spellings from the sections above. Git lowercases the final component of a key, so a user-chosen name is lowercased with it — an alias set as `worktrunk.config.aliases.Deploy` is invoked as `wt deploy`. Deprecated spellings may still deserialize, but git-sourced configuration does not run file migration or emit deprecation guidance — `wt config update` has nothing to rewrite here. Per-worktree git config (`extensions.worktreeConfig`) is only partly reachable: keys are read from the shared git dir, so a linked worktree's `config.worktree` is never consumed, while the main worktree's is — and then for the whole repository. Setting `WORKTRUNK_PROJECT_CONFIG_PATH` — even to an empty value — disables this source entirely; the override names the project config source outright.


To list the matching git keys with their scope and origin file (inside a linked worktree this can also show worktree-scoped keys, which worktrunk does not read):

```console
$ git config --show-scope --show-origin --get-regexp '^worktrunk\.config\.'
```

# Shell Integration

Worktrunk needs shell integration to change directories when switching worktrees. Install with:
Expand Down
24 changes: 11 additions & 13 deletions src/commands/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -785,9 +785,10 @@ fn render_aliases_help_section(
/// Callers (`augment_help`, `wt config alias show` with no name) latch
/// `suppress_warnings()` before reaching here so the standard `UserConfig::load()`
/// stays quiet: no deprecation warnings, no `.new` file writes, no
/// approved-commands copy. Project config is parsed directly from TOML rather
/// than via `ProjectConfig::load` because the `aliases` table has no deprecated
/// forms — skipping the migration avoids the unrelated warnings entirely.
/// approved-commands copy. Project config goes through `ProjectConfig::load`
/// so this listing reflects the same source selection as execution — in
/// particular the git-config source (`worktrunk.config.*`), whose aliases
/// must appear here exactly when dispatch would run them.
///
/// Tolerates missing or unloadable config: this is a discovery surface, not
/// an execution surface, so we'd rather show the built-in commands than
Expand Down Expand Up @@ -823,17 +824,14 @@ pub(crate) fn load_aliases_for_listing() -> Vec<(String, CommandConfig, HookSour
entries
}

/// Parse `.config/wt.toml` directly, extracting just `aliases`, without
/// triggering `ProjectConfig::load`'s deprecation warning and hint-writing
/// side effects. See `load_aliases_for_listing` for why.
/// Load project aliases through the standard source selector, tolerating
/// discovery-time errors. Callers latch `suppress_warnings()` (see
/// `load_aliases_for_listing`), which keeps `ProjectConfig::load` quiet.
fn load_project_aliases_silent(repo: &Repository) -> Option<BTreeMap<String, CommandConfig>> {
let path = repo.project_config_path().ok().flatten()?;
if !path.exists() {
return None;
}
let contents = std::fs::read_to_string(&path).ok()?;
let config: ProjectConfig = toml::from_str(&contents).ok()?;
Some(config.aliases)
ProjectConfig::load(repo, false)
.ok()
.flatten()
.map(|config| config.aliases)
}

#[cfg(test)]
Expand Down
19 changes: 13 additions & 6 deletions src/commands/config/approvals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,19 @@ fn collect_approvable_commands(project_config: &ProjectConfig) -> Vec<Approvable
/// semantics need the config as their frame of reference (`add`, `clear
/// --stale`); the read-only `list` instead treats absence as zero commands.
fn require_project_config(repo: &Repository) -> anyhow::Result<ProjectConfig> {
let config_path = repo
.project_config_path()?
.context("Cannot determine project config location — no worktree found")?;
Ok(repo
.load_project_config()?
.ok_or(GitError::ProjectConfigNotFound { config_path })?)
// Load before resolving a path: the git-config source (worktrunk.config.*)
// can supply project config when no worktree resolves a file path at all
// (bare repo, default branch checked out in no worktree). The path is
// needed only to frame the not-found error.
if let Some(config) = repo.load_project_config()? {
return Ok(config);
}
match repo.project_config_path()? {
Some(config_path) => Err(GitError::ProjectConfigNotFound { config_path }.into()),
None => anyhow::bail!(
"No project config found — no worktree resolves .config/wt.toml, and git config has no worktrunk.config.* keys"
),
}
}

/// One project command and whether its template is currently approved.
Expand Down
23 changes: 21 additions & 2 deletions src/commands/config/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::path::PathBuf;
use worktrunk::config::{ConfigFileKind, require_config_path};
use worktrunk::git::Repository;
use worktrunk::path::format_path_for_display;
use worktrunk::styling::{eprintln, hint_message, info_message, success_message};
use worktrunk::styling::{eprintln, hint_message, info_message, success_message, warning_message};

/// Example user configuration file content (displayed in help with values uncommented)
const USER_CONFIG_EXAMPLE: &str = include_str!("../../../dev/config.example.toml");
Expand Down Expand Up @@ -63,7 +63,26 @@ pub fn handle_config_create(project: bool) -> anyhow::Result<()> {
"See https://worktrunk.dev/hook/ for hook documentation",
],
user_config_exists,
)
)?;
// Born superseded: existing worktrunk.config.* keys in git config are
// the project config (all-or-nothing), so the file just created will
// not be read until they are removed. Say so now, not at first use.
if !repo.worktrunk_config_git_pairs()?.is_empty() {
eprintln!(
"{}",
warning_message(cformat!(
"<bold>worktrunk.config.*</> keys exist in git config; the new project config will be ignored until they are removed"
))
);
eprintln!(
"{}",
hint_message(cformat!(
"To list the keys and their origins, run <underline>{}</>",
worktrunk::config::GIT_CONFIG_LIST_COMMAND
))
);
}
Ok(())
} else {
let project_config_exists = Repository::current()
.and_then(|repo| repo.project_config_path())
Expand Down
Loading
Loading