Skip to content

fix(hermes): run-as-hal0 guard + ownership handover — prevent root-clobber (#843)#844

Merged
thinmintdev merged 3 commits into
mainfrom
fix/hal0-runas-guard
Jun 15, 2026
Merged

fix(hermes): run-as-hal0 guard + ownership handover — prevent root-clobber (#843)#844
thinmintdev merged 3 commits into
mainfrom
fix/hal0-runas-guard

Conversation

@thinmintdev

Copy link
Copy Markdown
Contributor

Closes #843.

Problem

hal0-managed processes must run as the unprivileged hal0 user, but nothing
stopped root from running Hermes (sudo hermes …, a root TUI, or root-context
bootstrap). When that happened: (1) ~/.hermes resolved to /root/.hermes
(split-brain tree the service never reads), and (2) files landed root:root
(config.yaml/runtime.json 0600, the venv) so the User=hal0 unit hit EACCES
— or Hermes silently fell back to the default provider. The regression kept
returning because past fixes were applied on the live box, not in the repo.

This generalises the fix into one primitive every hal0-managed process inherits.

Fix (three layers)

Layer 1 — prevent at entry

  • installer/lib/run-as-hal0.sh — sourced guard; when EUID==0 it re-execs the
    command as hal0 with the correct HOME and HERMES_HOME stripped
    (runusersetprivsudo -H; refuses loudly if none). HAL0_ALLOW_ROOT=1
    opts out for deliberate root debugging.
  • installer/wrappers/hermes sources it first; install.sh installs it to
    /usr/lib/hal0/guards/run-as-hal0.sh.
  • agent_shim._runas_popen_extras() drops the child to hal0 on the systemd
    ExecStart path (it execs the venv binary directly, bypassing the wrapper).

Layer 2 — hand artifacts to hal0 (also reconciles under --repair)

  • hermes_provision._chown_tree_to_hal0() chowns the venv, $HERMES_HOME tree,
    and runtime.json to hal0 when root; no-op otherwise.

Layer 3 — detect (read-only)

  • hal0 doctor perms audits Hermes ownership + flags a stray /root/.hermes,
    pointing at bootstrap --repair. Never auto-repairs (no silent chown -R).

Tests

TDD throughout (guard via sh with a runuser stub + HAL0_RUNAS_TEST_UID
seam; wrapper/install wiring; chown helper + phase wiring; shim drop; doctor
check). 563 passed in tests/cli + tests/agents; ruff + shellcheck clean.
(Full pytest tests/ hangs on the dev box per the known lemond gotcha — CI runs
the rest.)

Scope / notes

  • Design spec: docs/superpowers/specs/2026-06-15-hal0-runas-guard-design.md
    (with as-built notes). Operator runbook: docs/agents/hermes/SERVICE.md.
  • Preserves the 2775 setgid on /var/lib/hal0; only the .hermes subtree +
    venv ownership change.
  • Not deployed to CT105 (shared host — another session is active on that
    runtime). Deploy + sudo hal0 agent bootstrap hermes --repair will converge
    the live box.
  • Deferred follow-up: a root-context harness smoke row (real stat + no
    /root/.hermes) — can't run in non-root CI.

🤖 Generated with Claude Code

thinmintdev and others added 3 commits June 15, 2026 03:38
Add the approved design for a shared run-as-hal0 guard primitive that
prevents any hal0-managed process launched as root from clobbering agent
state (split-brain /root/.hermes home + root:root perm clobber). Three
layers: prevent (shared runas.sh re-exec + bootstrap chown), detect
(read-only drift check), repair (explicit bootstrap --repair reconcile).

Also document the operator remediation in SERVICE.md as a failure mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…obber (#843)

Prevent any hal0-managed process launched as root from writing a split-brain
/root/.hermes tree or root:root perms the User=hal0 unit can't read (the silent
"falls back to default provider" failure).

Layer 1 — prevent at entry:
* installer/lib/run-as-hal0.sh — sourced guard; when EUID==0, re-execs the
  command as hal0 with correct HOME and HERMES_HOME stripped (runuser →
  setpriv → sudo; refuses if none). HAL0_ALLOW_ROOT=1 opts out.
* installer/wrappers/hermes sources it first; install.sh installs it to
  /usr/lib/hal0/guards/run-as-hal0.sh.
* agent_shim.py _runas_popen_extras() drops the child to hal0 on the systemd
  ExecStart path (which execs the venv binary directly, bypassing the wrapper).

Layer 2 — hand artifacts to hal0 (also reconciles under --repair):
* hermes_provision._chown_tree_to_hal0() chowns the venv, HERMES_HOME tree, and
  runtime.json to hal0 when root; no-op otherwise.

Layer 3 — detect (read-only):
* `hal0 doctor perms` audits Hermes ownership and flags a stray /root/.hermes,
  pointing at `bootstrap --repair`. Never auto-repairs.

TDD: guard (shell via sh), wrapper/install wiring, chown helper + phase wiring,
shim drop, doctor check. 563 passed in tests/cli + tests/agents; ruff +
shellcheck clean. As-built notes appended to the design spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thinmintdev thinmintdev enabled auto-merge (squash) June 15, 2026 10:10
@thinmintdev thinmintdev merged commit f133d74 into main Jun 15, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hermes: prevent root from running it / clobbering perms (root-clobber regression)

1 participant