A desktop app for managing git worktrees across projects on macOS. Worktrees are tabs down the left,
details and a live terminal on the right, and the New Worktree form is defined by each project
itself in a wtm.toml — including dropdowns populated by your own shell commands (branch lists, Jira
issues via acli, anything that prints to stdout).
The app knows nothing about just, Jira, Docker, or any particular repo. It reads git and it runs the
commands your config declares. A repo with heavy worktree tooling and a bare library with none are the
same code path with different TOML.
Built with Tauri v2 + Rust + Svelte 5.
Status: personal tool, not distributed. macOS 13+, Apple silicon, unsigned .app.
| ✅ | Register any git repository; worktrees appear as tabs down the left |
| ✅ | Favorite a worktree to pin it to the top of the sidebar, under its own heading; persisted per project in ~/.config/wtm/config.toml |
| ✅ | Detail pane: branch, directory, HEAD, dirty/staged/untracked, ahead/behind, Jira key, copyable path |
| ✅ | Config-driven display — badges, links, and prefix-grouped tables (e.g. a port table) from wtm.toml |
| ✅ | The New Worktree form, generated from the project's config, with dropdowns populated by running the project's own commands |
| ✅ | Light / dark / follow-system theming, persisted to ~/.config/wtm/config.toml |
| ✅ | Config trust prompt: a project's declared commands are shown verbatim and never run until approved |
| ✅ | Diagnostics: the resolved PATH and which project tools are reachable |
| ✅ | Credential-bearing .env values are withheld from the UI entirely until revealed one at a time — see Environment values |
| ✅ | Pressing Create — renders the branch and directory, runs preflight, fetches, calls git worktree add, then the project's setup command in a live terminal |
| ✅ | A review screen showing the exact git worktree add and setup argv, plus the cwd, before anything happens |
| ✅ | Adopting an existing branch instead of creating one — the GUI form of the shell's numbered stdin picker |
| ✅ | Removing a worktree: the project's teardown steps, then git worktree remove, then optionally the branch |
| ✅ | Live terminal pane for setup output and ad-hoc [[action]]s, with input routed back so a prompt is answerable |
| ✅ | Live Claude Code, Codex, and Cursor Agent sessions with normalized transcripts, approvals, model controls, resumable conversations, and internal MCP handoff |
| ✅ | Claude's high-speed mode, from the composer's Fast pill or /fast — wtm drives the CLI over the Agent SDK protocol, so it declares the opt-in the SDK requires |
| ✅ | Voice dictation into the prompt composer — hold or tap the mic, audio goes to Deepgram Nova-3, transcript lands in the draft unsent. Off by default; the only feature that sends anything off your machine |
| ✅ | Cross-model delegation from one chat: one visible child or a customizable run of up to 20 child agents, with per-child model/effort/mode and navigable session status |
| ✅ | Open in … — a split button that hands the worktree to your editor, a terminal, the file manager, or a fresh Claude Code session; see below |
| ✅ | Browser panes — a real web page tiled beside your sessions, driveable by the agents in that worktree, with element-anchored comments you can hand to an agent; see below |
| 🚧 | [remove] strategy = "command" — the native path is the default and the one that turns the branch prompt into a checkbox |
| 🚧 | A command palette, and notify-based auto-refresh |
Verified end to end against a real repository, not just against fakes. Point the suite at your own
checkout and it creates a worktree — real issue-tracker lookup, real branch naming, real
git worktree add — asserts it against git, then removes it and deletes the branch:
WTM_TEST_REPO=~/code/myproject cargo test -p wtm-app -- --ignored --nocapture --test-threads=1Every expectation there is derived from your config, so it verifies wtm rather than any one project's convention. With the variable unset the tests skip.
Install · Updating · Prerequisites · Setup · First run · Registering a project · Writing wtm.toml · Browser panes · Open in … · Settings · Dev workflow · Build & install · Troubleshooting · Logs · Dependencies · Architecture
You do not need to clone this repository to use wtm. Everything below is the latest release.
brew install --cask takumihendricksdev/tap/wtmwtm is not code-signed or notarized, so macOS would refuse to open it and report it as "damaged and can't be opened" — which is Gatekeeper's phrasing for "nobody paid Apple to vouch for this", not a claim about the download. The cask therefore clears the quarantine attribute after installing.
That is a deliberate Gatekeeper bypass, and it should be a thing you know is happening
rather than a surprise. Homebrew used to expose --no-quarantine for exactly this case;
as of Homebrew 6 the flag is rejected and the HOMEBREW_CASK_OPTS path is dead code, so
a cask for an unsigned app has no supported opt-out left. If you would rather macOS made
the call, take the zip below instead of the tap.
brew update && brew upgrade --cask wtmbrew update is not optional here — it is what fetches the tap's new cask. Without it
Homebrew still holds the recipe it last saw and will report wtm as up to date whatever
has been released. The upgrade backs up the old .app, swaps in the new one, and re-runs
the quarantine bypass, so nothing about Gatekeeper needs doing a second time.
brew list --cask --versions wtm says which version you actually have, which is worth
checking against the latest release
if a feature you expect is missing.
There is no in-app updater and no update check — wtm will not tell you a new version exists.
To remove it: brew uninstall --cask wtm, or brew uninstall --zap --cask wtm to take
~/.config/wtm (your preferences, trust decisions and log) with it.
The zip is on the releases page
with a SHA-256 checksum. Fetch it with curl or gh rather than a browser —
browsers set the quarantine attribute, the CLI does not, so a CLI download of an unsigned
app opens without any of the above applying.
Everything from here on is about building it yourself.
| Tool | Version | Install |
|---|---|---|
| macOS | 13+, Apple silicon | — |
| Xcode Command Line Tools | any | xcode-select --install |
| Rust | pinned to 1.97.1 by rust-toolchain.toml |
see below — rustup, not a package manager |
| Node | 20.19+ / 22.12+ | brew install node |
| bun | 1.x | curl -fsSL https://bun.sh/install | bash |
just (optional) |
1.50+ | only needed by projects whose config calls it |
acli (optional) |
any | Atlassian CLI — only for Jira-backed form fields |
gh, docker (optional) |
any | only if a project's config uses them |
sox (optional) |
any | dictation only — brew install sox |
curl (optional) |
any | dictation only — already present on macOS |
Full Xcode is not required. Command Line Tools is enough for desktop Tauri.
Install Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimalThen restart your shell, or . "$HOME/.cargo/env".
Do not install Rust from a package manager — not
brew install rust, notapt install rustc. Those builds ignorerust-toolchain.toml, cannot add build targets, and upgrade themselves during unrelated system upgrades — which invalidatestarget/and costs you a full rebuild.just doctorwarns if a packaged rust is shadowing the rustup shims.
just setupIdempotent, safe to re-run. It pins the toolchain, adds rustfmt/clippy/the wasm target, installs
cargo-nextest/cargo-deny/bacon, installs frontend dependencies, and points
core.hooksPath at .githooks.
Then check your machine:
just doctordoctor reports each tool twice over: whether it's on your current PATH, and whether it's on your
login-shell PATH. That second check matters — see the PATH note below.
just dev⏱️ The first build compiles ~800 crates: 3–6 minutes on an M-series Mac, and
target/grows to 3–6 GB. Later runs are seconds. Frontend edits hot-reload; Rust edits trigger a partial rebuild and a window restart.
On first launch wtm creates ~/.config/wtm/config.toml. Open it with just config.
Point wtm at any git repository. It reads the repo with git worktree list --porcelain -z and shows
what's there — no config required, nothing written to the repo.
Config is resolved in four layers, most specific winning:
| Layer | Path | Committed? | Use it for |
|---|---|---|---|
| Local | $(git rev-parse --git-common-dir)/wtm.local.toml |
no — lives inside .git |
machine-specific overrides, or configuring a repo you don't own |
| Repo | <repo>/wtm.toml |
yes | the shared team convention |
| User | ~/.config/wtm/config.toml |
n/a | registered projects, theme, defaults for every project |
| Built-in | defaults/wtm.default.toml |
— | a working New Worktree form with zero configuration |
To try the bundled example against a repo without touching its tracked files:
just install-example REPO=~/code/myprojectThat writes wtm.local.toml into the repo's git directory, which is untracked and shared by all of
that repo's worktrees. Move it to a committed <repo>/wtm.toml when you want to share it with the
team. Pass CONFIG= to install your own file instead of the bundled example — useful for a config
that describes an internal project and should not live in this repository.
examples/webapp.wtm.toml is the fully-commented reference: a form with an
issue-tracker lookup, a computed slug, branch/directory naming templates, a PTY setup command, Docker
teardown steps, a per-worktree port table, and guards for the commands that cannot be run from a GUI.
The short version:
schema_version = 1
[project]
name = "some-lib"
[[field]] # → a text input on the New Worktree form
key = "name"
label = "What are you working on?"
kind = "text"
required = true
[[field]] # → a dropdown whose options come from a shell command
key = "base"
label = "Base"
kind = "select"
default = "HEAD"
[field.options]
kind = "command"
run = ["git", "for-each-ref", "--format=%(refname:short)", "refs/heads"]
cwd = "repo_root"
parse = "lines"
[naming] # → what gets created
branch = "{{ name | slugify }}"
directory = "{{ name | slugify | truncate(40, '') }}"
dir_base = "repo_parent"Everything else has a default. Field kinds are text, multiline, number, bool, select,
multiselect, path.
One template gotcha worth knowing. An undefined token is not equal to
'', so awhen = "env.FOO != ''"guard is true whenFOOis unset — the opposite of what it reads like. Writewhen = "env.FOO | default_if_empty('') != ''"instead.
wtm.tomlcan run shell commands and declare database targets. wtm will not use either from a project until you explicitly approve it, and it re-asks whenever the file's contents change. The prompt shows command argv and credential-free database targets; passwords and credential-bearing URLs are never included.
The Database view is attached to the selected worktree. It provides a schema/table tree, column metadata, paged and sortable table data, and an independent SQL console with selection execution, bounded results and cancellation. PostgreSQL and SQLite are supported; an engine not included in this build is shown as unavailable rather than attempted.
A local service whose port and credentials vary per worktree belongs in the repo config:
[database.local]
label = "Local database"
engine = "postgres"
scope = "worktree" # the default
environment = "local" # the default
host = "127.0.0.1"
port = "{{ env.DB_PORT }}"
name = "{{ env.DB_NAME }}"
user = "{{ env.DB_USER }}"
password = "{{ env.DB_PASSWORD }}"env.* comes from the worktree's existing [[display.source]] files, so selecting another
worktree resolves a different port and opens a different session. A file-backed repository is the
same shape with engine = "sqlite" and path = "var/app.sqlite3"; relative paths resolve inside
that worktree.
Shared TEST/STAGING/PROD connections use scope = "project". Their tabs and live session persist
while you move among that project's worktrees, but never into another project. Put machine-specific
definitions in the untracked, git-common wtm.local.toml; project-scoped profiles deliberately
cannot use env.*, because that would make one allegedly shared session depend on whichever
worktree happened to be selected when it connected.
[database.production]
label = "Production (read only)"
engine = "postgres"
scope = "project"
environment = "production"
access = "read_only"
url = "postgres://readonly-user:password@db.example.invalid/app"
tls = "require"Credentials are rendered and retained in Rust, never listed over IPC or logged. Read-only profiles are also enforced by the driver. A read/write production console has an additional per-session UI lock; table browsing remains available while it is locked.
A worktree's .env often holds real credentials, and this app displays that file. How it is
handled:
- The webview cannot reach the network. The CSP permits only
selfand IPC; database protocols and opt-in dictation live behind narrow Rust commands. There is no telemetry. - Nothing is logged. No log line carries an environment value.
- No value is sent to the window. Not "no secret" — no value. The listing carries key
names only; the Environment tab shows
••••••••for every row with a per-key reveal, which fetches that one value on demand, reads it fresh from disk, and re-masks when you switch worktrees. A screenshot or a screen-share cannot leak what was never sent.
There is deliberately no attempt to work out which keys are secrets. An earlier version
classified them by key name, by whether the value looked like scheme://user:pass@host, and
by whether a value matched another key's secret. It worked, and it was still the wrong shape:
guessing fails in two directions — under-match and a credential is published, over-match and a
port number needs a click — and every project's .env gets a vote on which way. The type the
listing uses can no longer hold a value at all, so this is a property of the design rather
than a policy that has to be kept correct.
cargo test -p wtm-app --test env_masking proves it, against a repo whose .env is nothing
but credentials. It runs as part of just check — it no longer needs a real checkout, because
the guarantee no longer depends on the data.
A browser pane is a real web page — the platform's WebKit, the same engine the app itself runs on —
tiled beside your shells and agent sessions in a worktree. Open one from the empty surface's
Browser button, the worktree bar, or a pane's Split control. New panes open the first visible,
openable HTTP(S) [[display.link]] in repository order, with templates resolved for that worktree.
Without an available link, the pane starts empty; type an address (a bare
localhost:5173 gets http://, anything else https://), or pick one of the worktree's
[[display.link]] URLs from the empty state. The pane has Back, Forward, Reload, an address bar
(⌘L), and opens the page in your real browser on request. It follows its tile when you split,
drag or resize, disappears while a dialog is up or another worktree is selected, and comes back
after a relaunch at the address it was on.
A project can override where new panes start with [browser].home:
[browser]
home = "http://localhost:{{ env.WEB_PORT }}" # rendered per worktree, like a display linkAgents can use it. Every agent session in the worktree gets mcp__wtm__browser_* tools:
browser_open, browser_list, browser_navigate, browser_snapshot (the page as an outline with
element refs — the primary way an agent reads a page), browser_click, browser_type,
browser_fill_form, browser_select_option, browser_press_key, browser_hover,
browser_scroll, browser_wait_for, browser_screenshot, browser_get_content,
browser_console, browser_evaluate, browser_read_comments, browser_resolve_comment and
browser_close. Ask a session to "open the dev server and check the signup form" and a pane
appears in the worktree; the element it acts on flashes, and the pane's header says who is driving.
Each pane has an Agent access: on/off toggle to allow or block that, Settings has a global
switch, and an agent can close only the panes it opened. Everything an agent reads from a page
arrives wrapped as untrusted web content — see ARCHITECTURE §6c for the whole trust story.
Comments. Turn on Comment in the pane's toolbar (⌘⇧C), hover to see what you would pick,
click an element and say what should change — "make this a blue button". A numbered pin stays on
the element, the comments list opens beside the page, and Send N to Claude drafts them into the
focused agent's composer with the element, its text and a CSS selector, for you to read and send.
Agents can also fetch them with browser_read_comments and mark them resolved.
What has been verified, and how. On macOS, driven from the app's own log rather than by a
hand on the mouse: a pane opened through the store's ordinary path is placed exactly over its tile
and moves with the layout; an agent-opened pane is adopted into the worktree; a pane is restored
after a relaunch and reloads its address; a remote page's attempt to invoke the app is refused
and its attempt to navigate onto tauri://localhost goes nowhere; and the tool sequence
open → snapshot → click (which navigated) → screenshot → console → evaluate → content → comments →
history → close ran against a live page and returned what it should. Not yet exercised by a person:
comment mode's in-page pins and popover, the comments panel, the hide-behind-a-dialog placeholder,
and the keyboard chords — all wired, none clicked.
Limits worth knowing. Clicks and keys an agent sends are synthesized DOM events, not OS input:
links, buttons, checkboxes, and framework handlers all work, but a native <select> popup, a file
chooser, or a window.open that needs a real user gesture will not (use browser_select_option
for selects). Downloads and popups are handed to your default browser and the same pane
respectively. Four browser panes per worktree, eight in all — each is a WebContent process.
A split button in the detail header. The left half hands the worktree's directory to your
preferred tool; the right half is a menu of everything wtm knows about. Picking one launches
it and makes it the default, stored as ui.opener in ~/.config/wtm/config.toml.
Supported: Claude Code (in a terminal, or handed to Claude Desktop), VS Code, Cursor, Windsurf, Zed, PyCharm, IntelliJ IDEA, WebStorm, Sublime Text, Fork, a terminal, and Finder / your file manager.
Tools you do not have are listed but disabled, with the reason — usually "no code on
wtm's PATH". That is deliberate: it doubles as a diagnosis of this app's most likely failure,
a GUI-launched process that cannot see your shell's PATH (see
Troubleshooting). On macOS a tool is found either by its shell command or
by its .app bundle, so VS Code works whether or not you ever ran Shell Command: Install
'code' command in PATH.
Two things worth knowing:
- The two Claude entries go to different places, and are detected separately. Claude
Session uses the
claude-cli://open?cwd=…deep link theclaudeCLI registers, and that handler starts a session in a terminal emulator — iTerm2 if you have it, otherwise Ghostty, Kitty, Alacritty, WezTerm or Terminal.app, in that order. It needsclaudeon wtm'sPATH. Claude Desktop usesclaude://code/new?folder=…, which the desktop app itself registers, and lands in the app with no terminal involved; it needs the app installed, not the CLI. Either can be offered without the other. - Claude Desktop asks to trust the folder the first time. That prompt is the app's own, once per directory, and wtm neither suppresses nor pre-answers it.
- Fork opens the worktree, not the repository it was cut from. Its CLI takes a command
rather than a path (
fork open), so wtm runs it with the worktree as the working directory — same mechanism the terminal opener uses. Verified against a real linked worktree, where.gitis a file rather than a directory: Fork resolves it correctly and lands on that worktree's branch. - Nothing here is a project config concern. Openers are built in and identical in every
repository, so they need no
wtm.tomlentry and trigger no trust prompt.[[action]]is still the place for a per-project button. The catalogue is compiled in, so adding a tool is currently a one-entry code change insrc-tauri/src/openers.rsrather than something you can do from a config file.
⌘, or the sliders button in the title bar. Everything applies as you change it; there is no OK button.
Three sections, all of them backed by keys that already existed in
~/.config/wtm/config.toml and were previously reachable only by editing that file:
| Section | What is in it |
|---|---|
| Appearance | Colour palette, and light / dark / follow-the-system |
| General | Which tool Open in … defaults to |
| Advanced | The PATH override, plus read-only diagnostics — the PATH wtm actually resolved, where it came from, and which of the common tools it can find |
Advanced is where to look first when a project's commands work in your terminal and not in wtm. See the PATH problem.
Nine ship with the app: Pine (the default), Clay (the terracotta wtm wore before v0.4), Slate (near-neutral, for no colour at all), Harbor, Plum and Rose. Each works in both light and dark.
The last three, Paper, Fog and Dusk, are softer. In dark mode they sit on
charcoal (about #232323) rather than near-black, and their accents are quieter. Paper
is cream and warm grey with an ink-blue accent, Fog is a cool blue-grey, and Dusk is
lavender. Pick one of these if the other six feel harsh over a long session.
If none of them suit, declare your own. It appears in the picker beside the built-in nine:
[ui.palettes.nord]
name = "Nord" # optional; the table key is used when absent
hue = 250 # oklch hue angle, 0–360
chroma = 0.9 # how strongly the greys are tinted. 1 is the reference, 0 is flat
brand = ["#88c0d0", "#81a1c1", "#5e81ac", "#4c688f"]hue and chroma are all the neutral ramp needs — every surface, border and text colour in
the app is derived from them in oklch, at lightness values fixed by the stylesheet. That is
what keeps a hand-written palette as readable as the built-in ones: you choose the hue, and
the contrast ratios are not yours to get wrong. A custom palette uses the lightness of the
original six, so its dark mode is the deep one.
brand is the accent, from lightest to darkest. Dark mode uses the first two and light
mode the last two, so pick the first pair to read against near-black and the last pair
against near-white — not to look good as a row of four.
A palette that cannot be used is still listed, greyed out, with the reason on hover. Bad hex,
a hue outside 0–360, or anything other than exactly four #rrggbb colours will do it. The
rest of your config still loads.
just dev # run the app with hot reload
just watch # bacon: check → clippy → test, in a second pane
just fmt # format Rust + web
just check # everything CI runs — do this before pushing
just audit # licenses + RUSTSEC advisories
just doctor # what's installed, and the PATH the app will actually use
just icon # redraw src-tauri/icons from assets/brand/wtm-icon.svg
just release 0.9.1 # test, tag, publish, and update the Homebrew tapjust release <version> starts only from a clean, up-to-date main. It updates every version
field, runs just check and just audit, pushes the release commit, and waits for CI before
creating the tag. The tag starts the release build; after the artifact passes its checksum and the
bundle inspection — right version, right minimum OS, an arm64 Mach-O inside — the command publishes
the draft and pushes the verified checksum to TakumiHendricksDev/homebrew-tap. A stopped command
can be run again with the same version to resume completed stages safely.
Commits are signed through the 1Password SSH agent (commit.gpgsign=true globally), so 1Password must
be running and unlocked or the commit will hang waiting on Touch ID.
The first commit in a fresh clone of this repo:
git add -A && git commit -m "Initial commit"just build # the .app (~35 s warm, ~70 s cold)
just run # build, then launch it
just install-app # build, then install it to /Applications
just build-dmg # ⚠️ prompts for Finder Automation permission the first timebuild produces target/release/bundle/macos/Worktree Manager.app, and install-app copies it
into /Applications and re-registers it with LaunchServices.
Warm is ~35s no matter how little you changed, because tauri build regenerates the context
wtm-app compiles against on every run, so that one crate always recompiles. That number used to be
minutes; ARCHITECTURE.md § Build performance has the measurements and what moved them.
CI builds the same bundle on every push, so a break in bundling is caught without anyone building it by hand.
The build is unsigned, by design — this is a personal tool.
- Built locally and copied locally: runs fine, no Gatekeeper prompt.
- Sent to someone else, or downloaded through a browser: macOS quarantines it and reports
"damaged and can't be opened." Fixing that means a paid Apple Developer account ($99/yr) plus
APPLE_SIGNING_IDENTITY/APPLE_CERTIFICATE/APPLE_CERTIFICATE_PASSWORDfor signing andAPPLE_ID/APPLE_PASSWORD/APPLE_TEAM_IDfor notarization. Ad-hoc signing (identity-) still leaves the recipient approving it under Privacy & Security. - Universal binary:
rustup target add x86_64-apple-darwin && just build-universal. Roughly doubles build time.
| Symptom | Cause | Fix |
|---|---|---|
App works under just dev, then "program not found" once installed |
A .app launched from Finder inherits PATH=/usr/bin:/bin:/usr/sbin:/sbin. just, acli, docker, bun all live in /opt/homebrew/bin. |
wtm probes your login shell's PATH at startup and uses it for every spawn. If a tool is still missing, set exec.path in ~/.config/wtm/config.toml. just doctor flags any tool that isn't on the login PATH. |
just: command not found |
/opt/homebrew/bin missing from a non-login shell (common in editor terminals and launchd) |
brew install just, and add /opt/homebrew/bin to PATH in .zprofile |
error: rustup could not choose a version of cargo |
rustup absent, or a Homebrew rust shadowing the shims |
Install via rustup; which -a cargo must list ~/.cargo/bin/cargo first |
First just dev seems hung |
It isn't — ~800 crates, with long silences on tao, wry, objc2-app-kit |
Wait 3–6 minutes. just watch in another pane shows progress on our own crates. |
| Jira fields come back empty, form still works | acli not authenticated or offline. Lookups are on_error = "warn", so fallbacks apply and creation is never blocked. |
acli jira auth login --web |
svelte-check errors about the TypeScript version |
typescript@7 is latest but svelte-check peers ^5 || ^6 |
TypeScript is pinned to ~6.0.3 in package.json — don't bump it to latest |
vite build fails with "Failed to load transformWithEsbuild" |
Vite 8 uses Rolldown/Oxc; the esbuild minifier is now a separate install | vite.config.ts sets minify: 'oxc'. Don't change it back to 'esbuild'. |
The microphone prompt comes back after every just build |
wtm is unsigned, so macOS keys the microphone grant to an ad-hoc code signature that changes on each rebuild | Expected, and only affects builds you make yourself. Approve it again; an installed copy that you stop rebuilding keeps its grant. |
Dictation says it needs rec |
SoX is not on the resolved PATH. rec is SoX's recording front-end and ships with it. |
brew install sox, then reopen Settings so the check re-runs. See the PATH problem if it is installed and still not found. |
| Dictation inserts nothing and says nothing was recorded | The microphone is muted or another application holds it. wtm records perfect silence happily and the service accepts it. | Check the input device in System Settings → Sound, and that no other app is recording. |
| The transcription key is rejected | The key is for a different Deepgram project, or was pasted with surrounding whitespace | Re-paste it in Settings → Advanced. wtm trims it, but a key copied with a line break from a terminal can pick up more than whitespace. |
| The app window opens behind another app | A bare binary launched from a shell does not activate | Use just run, or open "…/Worktree Manager.app" — a bundled app activates properly |
open fails with error -600 |
Rebuilding over the same bundle path leaves LaunchServices holding a stale record | just run re-registers the bundle first. By hand: lsregister -f "…/Worktree Manager.app" |
@tauri-apps/cli "cli-darwin-arm64 not found" |
bun didn't resolve the platform-specific optional dependency | rm -rf node_modules bun.lock && bun install |
| Setup command hangs forever with no output | The project's command is prompting on stdin, and a confirm()-style helper can loop forever on EOF rather than giving up. |
Every captured command has a mandatory timeout; PTY commands are interactive — answer in the Terminal tab, or Cancel. Add the command to [[guards.forbid]] so it can't be run again. |
| Worktree list is missing a worktree you just deleted by hand | git keeps stale admin entries until pruned | Refresh; wtm prunes on load. Or git worktree prune. |
| The app quit, or something failed with no visible reason | A .app launched from Finder has no stderr anyone can read |
just logs tails ~/.config/wtm/wtm.log, which every run appends to — including panics. WTM_LOG=debug (RUST_LOG grammar) turns up the detail. |
| A checkbox in New Worktree seems to have no effect on what runs | Nothing, now — but this was a real bug | Confirm on the review screen, which shows the exact setup argv wtm will run. If a [[setup.args_when]] flag appears when its box is unticked, that's a bug worth reporting. |
Every run appends to ~/.config/wtm/wtm.log, and also writes to stderr when you're running
just dev. This exists because a bundled macOS app has nowhere else to put a diagnostic: launched
from Finder, its stderr goes nowhere.
just logsPanics are logged too, and a panic inside a command surfaces in the UI as an error rather than
killing the app — [profile.release] deliberately does not set panic = "abort", and a test
enforces that.
just auditRuns cargo deny check over the Rust tree and bun audit over the frontend: RUSTSEC advisories,
license allow-list, duplicate versions, and source registries. Kept out of just check because it
refreshes the advisory database over the network, and a check that fails on a train is a check people
stop running.
Two things the config decides deliberately, both written down in deny.toml:
-
Vulnerabilities fail. Always. cargo-deny removed the option to downgrade them, which is the right call and what this gate relies on.
-
unmaintainedis scoped to crates this workspace chose. Seventeen unmaintained advisories come through Tauri:unic-*viaurlpattern, whose advisory says outright that no safe upgrade exists. Denying those would mean a permanent ignore list by advisory ID, which is where a real advisory goes to hide. If we add an unmaintained crate, it still fails.The count used to be seventeen, ten of them gtk-rs crates Tauri pulls on Linux. Those are gone now — not because the dependency tree changed but because
deny.tomlnames[targets], so cargo-deny evaluates the two Apple targets this app is built for instead of the union of every platform in the lockfile. A lockfile is a union; a shipped binary is not.
One version is pinned rather than current: TypeScript is held at ~6.0.3 because
svelte-check@4.7.4 peers ^5 || ^6. latest is 7.x and would break the type gate.
Attack surface is deliberately small — worth knowing when judging the dependency list. wtm makes no
network requests of its own: no HTTP client crate is in the tree, no fetch/XHR/WebSocket appears in
the frontend, and the webview CSP admits no remote origin. Everything it runs comes from a config
file you approved (see the trust prompt) or from git.
Design decisions, the crate layout, the ports, and why the toolchain is pinned the way it is live in ARCHITECTURE.md.
