Repository navigation
feat(doctor): check integrations with seats doctor, set up the herdr plugin in one step and tidy shared hooks - #22
Merged
Merged
Conversation
…d copy A hook installed from inside a seat, such as herdr's with CLAUDE_CONFIG_DIR set, wrote that seat's path into the shared Claude settings.json, so every seat ran it through that seat's folder (breaking when the seat is renamed or removed) and the same hook could run once per seat. seats sync now points hook and status line commands that reach a shared item through a seat folder, the shared folder or a seat that's gone at the live path (~/.claude/hooks/...), which herdr also writes from a plain shell and recognises as its own, so its next install adds no copy. Duplicate hooks in an event become one. Paths to an item a seat keeps out stay as they are. --dry-run lists the file.
seats hook herdr ran on every status change of every Claude Code and Codex pane and searched each seat's session folders (Codex's recursively) before noticing it had already reported that command. It now remembers the session it reported per pane and only searches again when the pane's session changes or herdr detects a new agent process in it, as on a restore or when an auto-switch reopens the conversation on the next seat. State saved by the previous version still reads.
seats install plugin herdr links plugins/herdr from a checkout, or from an installed binary runs herdr plugin install gelzinn/seats/plugins/herdr pinned to the installed version's tag. It stops with a clear message when herdr or seats isn't on the PATH, leaves a plugin herdr already has, and prints the herdr command with --dry-run. seats uninstall plugin herdr unlinks or uninstalls it. seats install plugin paseo prints the Paseo config to add, without editing it.
seats doctor prints a short list of what the session hook and the herdr and Paseo plugins need, with the command that fixes each problem, and exits 1 when something does: seats on the PATH and at this version, the session hook in the shared Claude settings (and which seats keep their own), hooks there that run through one seat, herdr's version, the seats plugin in herdr (enabled, linked from a folder that still exists), herdr's Claude and Codex integrations, and a Paseo config still loading the plugin from integrations/paseo. It never writes anything.
… on the new seat Right after an auto-switch both seats hold the same copy of the conversation, mtime included, so the search on detection can still pick the old seat. A detection now reports as before but leaves the pane's session unsettled, and the next status change searches once more, when the new seat has written to its copy.
gelzinn
force-pushed
the
feat/setup-doctor
branch
from
October 28, 2026 15:47
c69af6b to
4d3fdd5
Compare
This was referenced Oct 28, 2026
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.
Description
On a real machine the shared Claude
settings.jsonran herdr's hook three times (~/.config/seats/claude/1/hooks/…,~/.claude/hooks/…,~/.config/seats/claude/4/hooks/…), becauseherdr integration install claudewrites$CLAUDE_CONFIG_DIR/hooks/…and was run from inside seats. Every session reported three times, and renaming or removing seat 1 or 4 broke that entry for every seat. Setting up the herdr plugin also meant knowingherdr plugin link <path>. Nothing told you whether the session hook, the plugin or herdr's integrations were in place. Andseats hook herdrsearched every seat's session folders (recursively for Codex) on every pane status change.src/hook-paths.ts,sessionHook.tidy). On sync, hook and status line commands in the shared settings that reach a shared item by way of one seat get one path per item, and hooks that became the same are dropped. Rewritten: a seat's path to the item when it's linked to the shared copy (or is a real copy sync is about to fold in), the shared folder's path, and a path into a store seat folder that no longer exists. A seat's own copy of an item it keeps out is left alone.--dry-runlists the file.~/.claude/hooks/…) and not the shared folder's path. In herdr's source (src/integration/claude_settings.rs,command.rs,env.rs),integration install clauderecognises its hook only by the exact stringbash '<dir>/hooks/herdr-agent-state.sh' session, with<dir>fromCLAUDE_CONFIG_DIRor~/.claude, no symlinks resolved. It removes matching entries and keeps one canonical entry (its matcher,timeout: 10).integration statusonly reads the hook script's version, never settings.json. A shared-folder path is a form herdr never writes, so its next install from a plain shell would add a second entry. The live path is exactly what herdr writes from a plain shell, and it leads to the shared copy whichever seat is active. When the active seat keeps its own copy of the item, the shared folder's path is used instead.*,""or none) wins. Otherwise the first getsa|b, but only when that entry runs nothing else; if not, both stay. herdr's next install swaps a*entry for its own canonical entry, without adding a copy.src/providers/claude/session-hook.ts; the generic part only builds the path rewrite.sharedSettingsFilewas pulled out ofaddSessionHookinsrc/trust.tsso both use it.seats install plugin herdr/seats uninstall plugin herdr. This keeps theinstall|uninstall <kind>shape ofmcpandskill. From a checkout (plugins/herdr/herdr-plugin.tomlnext to the code) it runsherdr plugin link <checkout>/plugins/herdr. From the compiled binary ordist/, where that folder isn't there, it runsherdr plugin install gelzinn/seats/plugins/herdr --ref v<version> --yes. It first checks thatherdrandseatsare on PATH (herdr runsseats hook herdrfrom its server's PATH), leaves a plugin herdr already has, and prints the command with--dry-run. Uninstall runsunlinkfor a linked plugin anduninstallfor one from GitHub (source.kindinplugin list --json), as herdr requires.seats install plugin paseoonly prints the~/.paseo/config.jsonentry and never edits it.seats doctor. Read-only checks, printed as✓/!/-(skipped), with a fix under each problem, exit 1 when anything needs fixing:seatson PATH, and at this same version (new: an olderseatsthere lackshook session/hook herdr);settings.json;min_herdr_version;claude/codexintegrations, for tools that have seats.CLAUDE_CONFIG_DIRandCODEX_HOMEare removed first, so herdr looks where it would from a plain shell;plugins.<name>.pathstill ending inintegrations/paseo, with the new path.seats hook herdr. State per pane is now{ session, command }; the old string state still reads. A status change on the session already settled for the pane only costsherdr pane get, since herdr'sagent_status_changedpayload carries no session id. The seats are searched when the pane's session changes, and onpane.agent_detected. A detection always reports, as before, and leaves the session unsettled, so the next status change searches once more. Right after an auto-switch both seats hold the same copy, mtime included (copyTranscriptkeeps timestamps), and the search on detection could still pick the old seat. herdr firespane.agent_detectedwhen the pane's agent label changes or the agent is released (app/api.rs,emit_pane_state_update), so a new claude process after an auto-switch is covered.bun run check: 281 tests. New:test/hook-paths.test.ts,test/commands/doctor.test.ts(including that it writes nothing),test/commands/plugin.test.ts, more intest/herdr.test.ts. They usetempHome()and a fakeherdron PATH (test/helpers/fake-herdr.ts).env -i, throwaway HOME and XDG dirs, its own server). Reproduced the triple entry by runningherdr integration install claudefrom seat 1, plain, and seat 4. Then: sync, herdr install from a plain shell (no change), sync (no change), herdr install from seat 4 (adds its path), sync (collapses it), herdr install from a plain shell (no change). One round converges.seats install plugin herdrlinked andseats uninstall plugin herdrunlinked against that server.herdr plugin install gelzinn/seats/plugins/herdr --ref feat/session-hook --yesinstalled, and uninstall removed it.seats doctorshowed all ✓ afterwards.hooksandstatusLineonly (noenv), with the same seat layout, in a throwaway HOME. The three herdr entries became one*entry on~/.claude/hooks/…, the status line moved from seat 1's path to~/.claude/statusline-command.sh, and a second sync was a no-op.session list,plugin list), and both were unchanged before and after.--ref v0.4.0fails today: that tag has noplugins/herdr. The GitHub install works from the next release, and a checkout links instead.settings.jsonis shared,--dry-rundoesn't list the tidy-up yet.hooks/changes what~/.claude/hooks/…reaches until the next sync.herdr integration installrun inside a seat still adds that seat's path; the next sync collapses it.User-facing changes
BEGIN_COMMIT_OVERRIDE
feat(doctor):
seats doctorchecks the session hook,seatson PATH, the herdr plugin and integrations and Paseo's plugin path, with a fix for each problemfeat(install):
seats install plugin herdrsets up the herdr plugin in one step, andseats uninstall plugin herdrremoves itfeat(sync):
seats syncpoints shared Claude hooks that ran through one seat's folder at~/.claude/...and keeps one of each, so herdr's hook no longer runs once per seatperf(herdr): the herdr plugin only searches the seats when a pane's conversation changes or a new agent starts in it
END_COMMIT_OVERRIDE
Type of change
Checklist
bun run checkpasses (Biome lint and format, typecheck, tests)bun src/cli.ts helpruns without errors--dry-runor a throwawayHOMEsrc/providers/<id>/and is listed insrc/providers/index.ts