Harden Hub startup and display-name handling

- Display names starting with '-' were parsed by tmux rename-window as
  flags. The alias was persisted before tmux rejected it, so every later
  start/refresh/open failed in refresh_labels until aliases.json was
  edited by hand. Guard rename-window with '--', apply the name before
  persisting it, and roll back the in-memory table on failure.
- Start the Hub tmux server with -f /dev/null by default. Reading the
  user's ~/.tmux.conf let theme status-formats replace the Sessions bar
  and root-table bindings leak into Herdr; HERDR_HUB_TMUX_CONFIG still
  opts into a custom file.
- An unreadable aliases.json blocked start/refresh/open/choose. Degrade
  to default names with a warning (stderr, or the popup message) and
  leave the file untouched for `doctor` to report.
- `doctor` crashed with a traceback when a *_BIN override pointed at a
  missing executable; report it as a failure instead.
- Refuse to run inside another tmux server before discovery and server
  creation, so no unattached Hub is left behind.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 02:58:45 +00:00
co-authored by Claude Fable 5.1
parent 6b906de476
commit ebbd5821fd
4 changed files with 184 additions and 21 deletions
+88 -21
View File
@@ -47,6 +47,9 @@ from .selector import (
DEFAULT_SOCKET_NAME = "herdr-hub"
DEFAULT_TMUX_SESSION = "hub"
# The Hub server is configured entirely by configure(); reading the user's
# ~/.tmux.conf would let theme status-formats and root-table bindings leak in.
DEFAULT_TMUX_CONFIG = "/dev/null"
DEFAULT_SESSION_NAME = "default"
BOOTSTRAP_WINDOW_NAME = "__herdr_hub_bootstrap__"
WINDOW_KEY_OPTION = "@herdr_hub_key"
@@ -414,7 +417,7 @@ def discover_remote_processes(
raw = cmdline_path.read_bytes()
except (FileNotFoundError, ProcessLookupError):
continue
except (PermissionError, OSError):
except OSError:
unreadable += 1
continue
endpoint = parse_remote_argv(decode_cmdline(raw))
@@ -749,7 +752,10 @@ class TmuxHub:
truncate_label(alias, 32) if alias else window.endpoint.short_label
)
if window.name != display_label:
self.run(["rename-window", "-t", window.window_id, display_label])
# A display name may start with '-', which tmux would otherwise
# parse as a flag. set-option takes the value positionally and
# does not accept '--', so only rename-window needs the guard.
self.run(["rename-window", "-t", window.window_id, "--", display_label])
self.run(
[
"set-option",
@@ -763,13 +769,22 @@ class TmuxHub:
@serialized_hub_mutation
def apply_alias(self, key: str, label: str) -> ManagedWindow | None:
previous = self.aliases.get(key)
if label:
self.aliases[key] = label
else:
self.aliases.pop(key, None)
window = self.window_for_key(key)
if window is not None:
self._apply_window_label(window)
try:
self._apply_window_label(window)
except HubError:
# Keep the in-memory table consistent with what tmux shows.
if previous is None:
self.aliases.pop(key, None)
else:
self.aliases[key] = previous
raise
return self.window_for_key(key)
def dismissed_keys(self) -> set[str]:
@@ -1065,12 +1080,9 @@ class TmuxHub:
time.sleep(min(0.05, remaining))
def attach(self) -> None:
if inside_any_tmux():
if inside_socket(self.socket_name):
return
raise HubError(
"already inside another tmux server; detach from it before attaching to herdr-hub"
)
if inside_socket(self.socket_name):
return
ensure_not_nested(self.socket_name)
result = self.run(
["attach-session", "-t", self.session_name],
check=False,
@@ -1117,20 +1129,50 @@ def invocation_script_path() -> str:
return str(invoked)
def configured_socket_name() -> str:
return os.environ.get("HERDR_HUB_SOCKET", DEFAULT_SOCKET_NAME)
def configured_tmux_config() -> str:
override = os.environ.get("HERDR_HUB_TMUX_CONFIG")
return os.path.expanduser(override) if override else DEFAULT_TMUX_CONFIG
def load_alias_table() -> tuple[dict[str, str], str | None]:
"""Load display names, degrading to defaults when the file is unreadable.
A corrupt optional file must not block start/refresh/open; `doctor`
reports the problem and the warning is surfaced to the caller.
"""
registry = make_alias_registry()
try:
return registry.load(), None
except AliasRegistryError as error:
return {}, f"display names unavailable, using defaults: {error}"
def make_hub(
arguments: argparse.Namespace,
*,
load_aliases: bool = True,
warnings: list[str] | None = None,
) -> TmuxHub:
tmux_bin = resolve_binary("tmux", os.environ.get("HERDR_HUB_TMUX_BIN"))
ssh_bin = os.environ.get("HERDR_HUB_SSH_BIN") or shutil.which("ssh") or "ssh"
aliases = make_alias_registry().load() if load_aliases else {}
aliases: dict[str, str] = {}
if load_aliases:
aliases, alias_warning = load_alias_table()
if alias_warning is not None:
if warnings is not None:
warnings.append(alias_warning)
else:
print(f"herdr-hub: warning: {alias_warning}", file=sys.stderr)
return TmuxHub(
tmux_bin=tmux_bin,
ssh_bin=ssh_bin,
socket_name=os.environ.get("HERDR_HUB_SOCKET", DEFAULT_SOCKET_NAME),
socket_name=configured_socket_name(),
session_name=os.environ.get("HERDR_HUB_SESSION", DEFAULT_TMUX_SESSION),
config_file=os.environ.get("HERDR_HUB_TMUX_CONFIG"),
config_file=configured_tmux_config(),
script_path=invocation_script_path(),
aliases=aliases,
)
@@ -1199,7 +1241,17 @@ def catalog_windows(windows: Iterable[ManagedWindow]) -> list[CatalogWindow]:
]
def ensure_not_nested(socket_name: str) -> None:
if inside_any_tmux() and not inside_socket(socket_name):
raise HubError(
"already inside another tmux server; detach from it before attaching to herdr-hub"
)
def command_start(arguments: argparse.Namespace) -> int:
# Fail before discovery and server creation: a Hub nobody can attach to
# would otherwise be left running in the background.
ensure_not_nested(configured_socket_name())
herdr_bin = resolve_herdr()
report = discover_endpoints(herdr_bin, configured_proc_root())
if warning := remember_remote_endpoints(report.endpoints):
@@ -1280,7 +1332,8 @@ def command_open(arguments: argparse.Namespace) -> int:
def command_choose(arguments: argparse.Namespace) -> int:
hub = make_hub(arguments)
startup_warnings: list[str] = []
hub = make_hub(arguments, warnings=startup_warnings)
if not hub.exists():
raise HubError("herdr-hub is not running")
windows = hub.managed_windows()
@@ -1298,9 +1351,17 @@ def command_choose(arguments: argparse.Namespace) -> int:
return catalog_windows(hub.managed_windows())
def rename_session(endpoint_key: str, alias: str) -> None:
alias_registry.set(endpoint_key, alias)
# Apply to tmux first: a name tmux rejects must never be persisted,
# or every later start/refresh would fail while re-applying it.
previous = hub.aliases.get(endpoint_key, "")
if hub.apply_alias(endpoint_key, alias) is None:
hub.apply_alias(endpoint_key, previous)
raise HubError("the selected Session is no longer open")
try:
alias_registry.set(endpoint_key, alias)
except AliasRegistryError:
hub.apply_alias(endpoint_key, previous)
raise
def delete_session(
target: str,
@@ -1408,7 +1469,7 @@ def command_choose(arguments: argparse.Namespace) -> int:
delete_session_handler=delete_session,
stop_session_handler=stop_session,
)
startup_warnings = [*local_report.warnings]
startup_warnings.extend(local_report.warnings)
if registry_warning:
startup_warnings.append(registry_warning)
if startup_warnings:
@@ -1556,12 +1617,18 @@ def command_doctor(arguments: argparse.Namespace) -> int:
failures += 1
continue
version_args = [path, "-V"] if binary in {"tmux", "ssh"} else [path, "--version"]
result = subprocess.run(
version_args,
capture_output=True,
text=True,
check=False,
)
try:
result = subprocess.run(
version_args,
capture_output=True,
text=True,
check=False,
timeout=10,
)
except (OSError, subprocess.SubprocessError) as error:
print(f"{binary}: cannot run {path} ({error})")
failures += 1
continue
version = (result.stdout or result.stderr).strip()
print(f"{binary}: {version or path}")
registry = make_registry()