Open Ghostty into herdr, and never into nothing - #100
Merged
Conversation
Bare `herdr` already launches or attaches -- it starts the headless server
if one is not running and attaches to the existing session if one is. So
this adds no status check; the decision stays herdr's.
What the wrapper adds is the three things herdr cannot do for itself when
the GUI starts it rather than a shell:
1. Find the binary. A GUI launch inherits none of the PATH that
homebrew/homebrew.zsh and system/path.zsh build -- `PATH=/usr/bin:/bin
sh -c 'command -v herdr'` fails -- so `initial-command = herdr` would
not have resolved. Same problem and same fix as the prefix loop in
run_onchange_before_10_homebrew.sh.tmpl.
2. Not lock you out. Ghostty closes the window when its command exits, so
every path here ends at `exec "${SHELL:-/bin/zsh}" -l`. herdr runs as a
child, not exec: detaching leaves a prompt in the same window and a
crash leaves its output on screen.
3. Not nest. herdr exports HERDR_ENV in every pane and its allow_nested is
false by default.
`initial-command`, not `command`, so new windows and Cmd-T tabs stay plain
shells. The cost, per Ghostty's docs: it fires only for the first surface
ever created, so a window opened after that one closes is a plain shell
until Ghostty restarts.
The config becomes a template because the path must be absolute and must
point at the chezmoi SOURCE directory -- bin/ is in .chezmoiignore and runs
in place, so no copy of the script exists under $HOME.
herdr_candidates is a function rather than a constant so tests can redirect
it; left alone, the "herdr is not installed" test would find the real
/opt/homebrew/bin/herdr and attach the suite to a live session. The suite
also unsets HERDR_ENV, since it is normally run from inside a herdr pane --
without that every test took the nesting guard and passed for the wrong
reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JyYEk73g3uJoifwcSwj9Bg
jfmercer
force-pushed
the
ghostty-launches-herdr
branch
from
August 2, 2026 17:24
eecf309 to
c9423dc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Opening Ghostty now lands in herdr: the server starts if it is not running, and attaches to the existing session if it is.
The decision is herdr's, not ours. Bare
herdris documented as "Launch or attach to the persistent session," so there is noherdr statuscheck here to duplicate and later disagree with it.Why a wrapper at all
Three things herdr cannot do for itself when the GUI starts it rather than a shell:
PATHthathomebrew/homebrew.zshandsystem/path.zshbuild —PATH=/usr/bin:/bin sh -c 'command -v herdr'fails — soinitial-command = herdrwould not have resolved on this machine.find_herdrfalls back to a prefix loop, the same problem and fix as inrun_onchange_before_10_homebrew.sh.tmpl.exec "${SHELL:-/bin/zsh}" -l: herdr missing, herdr failing, and herdr detaching (ctrl-a q) all leave a usable prompt. herdr runs as a child, notexec, so a crash leaves its output on screen.HERDR_ENVin every pane and itsallow_nestedis false by default.HERDR_AUTOSTART=0is the manual escape hatch.Two deliberate trade-offs
initial-command, notcommand, so new windows andCmd-Ttabs stay plain shells. Per Ghostty's own docs it fires only for the first surface ever created — close that window and the next one is a plain shell until Ghostty restarts. Accepted, not overlooked.shell-integrationleft atdetect. Forcingzshwould give the fallback shell Ghostty's cursor/title features, but it injectsZDOTDIR, and since no shell runs before herdr to restore it, that would leak into the herdr server and every pane's shell — changing every pane to fix cosmetics on a rescue path. Noted inCLAUDE.mdso it does not get "fixed" later.Why the config became a template
The path must be absolute and point at the chezmoi source directory:
bin/is in.chezmoiignoreand runs in place, so no copy of the script ever exists under$HOME. AhomeDirpath would render fine and then never resolve. An invariant test asserts both halves.Tests
tests/bats/ghostty_session.bats(12 tests) defends the boring property — every path ends at a login shell — plus two cross-file invariants tying the Ghostty config to the script.herdr_candidatesis a function rather than a constant so tests can redirect it; left alone, the "herdr is not installed" test would find the real/opt/homebrew/bin/herdrand attach the suite to a live session. The suite also unsetsHERDR_ENVinsetup: it is normally run from inside a herdr pane, and without that every test took the nesting guard and passed for the wrong reason (which is exactly what happened on the first run).Three new rows in
tests/README.md's breakage table, each verified red:"$herdr" || rc=$?→"$herdr"exec_login_shellVerification
./tests/rungreen — 83 bats, 56 unittest, nothing skippedshellcheck bin/ghostty-sessionclean; it is picked up by CI's shebang-basedbin/*selection with no workflow changeghostty +validate-configexits 0 on the rendered configfind_herdrresolves/opt/homebrew/bin/herdrunderenv -i PATH=/usr/bin:/bin— the point of item 1 abovechezmoi applyrun, leaving only the four permanently-pendingrun_afterscripts inchezmoi statusVerified end to end after the fact: Ghostty quit and reopened attaches to the existing session; and with
herdr server stopfirst, a reopen launches a fresh server. Both branches confirmed on a real machine. ThePATHcaveat is closed too — a pane from the wrapper-launched server has the fullPATH(Homebrew, asdf shims,$DOTFILES/bin), since pane login shells rebuild it regardless of what the server inherited.🤖 Generated with Claude Code
https://claude.ai/code/session_01JyYEk73g3uJoifwcSwj9Bg