fix(hermes): run-as-hal0 guard + ownership handover — prevent root-clobber (#843)#844
Merged
Conversation
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>
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.
Closes #843.
Problem
hal0-managed processes must run as the unprivileged
hal0user, but nothingstopped root from running Hermes (
sudo hermes …, a root TUI, or root-contextbootstrap). When that happened: (1)
~/.hermesresolved to/root/.hermes(split-brain tree the service never reads), and (2) files landed
root:root(
config.yaml/runtime.json0600, the venv) so theUser=hal0unit 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 thecommand as
hal0with the correctHOMEandHERMES_HOMEstripped(
runuser→setpriv→sudo -H; refuses loudly if none).HAL0_ALLOW_ROOT=1opts out for deliberate root debugging.
installer/wrappers/hermessources it first;install.shinstalls it to/usr/lib/hal0/guards/run-as-hal0.sh.agent_shim._runas_popen_extras()drops the child tohal0on the systemdExecStart 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_HOMEtree,and
runtime.jsontohal0when root; no-op otherwise.Layer 3 — detect (read-only)
hal0 doctor permsaudits Hermes ownership + flags a stray/root/.hermes,pointing at
bootstrap --repair. Never auto-repairs (no silentchown -R).Tests
TDD throughout (guard via
shwith arunuserstub +HAL0_RUNAS_TEST_UIDseam; 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 runsthe rest.)
Scope / notes
docs/superpowers/specs/2026-06-15-hal0-runas-guard-design.md(with as-built notes). Operator runbook:
docs/agents/hermes/SERVICE.md.2775setgid on/var/lib/hal0; only the.hermessubtree +venv ownership change.
runtime). Deploy +
sudo hal0 agent bootstrap hermes --repairwill convergethe live box.
stat+ no/root/.hermes) — can't run in non-root CI.🤖 Generated with Claude Code