fix: hivectl choom enters as the agent user from the state dir so claude gets creds + session

This commit is contained in:
damocles 2026-06-05 21:41:50 +02:00
commit c59a0de01e
3 changed files with 62 additions and 17 deletions

View file

@ -279,7 +279,9 @@ Stop and restart ALL managed agent containers in sequence. Iterates the live con
Open an interactive Claude session inside an agent container.
Replaces the current process with `machinectl shell h-<name>` running `claude --continue` — drops the operator straight into the agent's live Claude session with its full loaded context and persona. Requires root (same as all machinectl shell operations) and the container must be running.
Replaces the current process with `machinectl shell <name>@h-<name>` running `claude --continue` from the agent's state dir — drops the operator straight into the agent's live Claude session with its full loaded context and persona. Requires root (same as all machinectl shell operations) and the container must be running.
The session is entered **as the agent user** (not root): claude reads its OAuth credentials + settings from the agent's `/home/<name>/.claude`, and the working directory is set to the agent's state dir (`/agents/<name>/state`) so `--continue` resumes the same per-project session the harness runs. Entering as root (the `machinectl shell` default) is what loses both.
Pass `--fresh` to start a new Claude session instead of continuing the most recent one.

View file

@ -111,9 +111,9 @@ reported at the end rather than aborting mid-run.
## Choom
Drop into an interactive Claude session inside an agent container.
Replaces the current process with `machinectl shell h-<name>` running
`claude --continue`. Requires root (same as all `machinectl shell`
operations).
Replaces the current process with `machinectl shell <name>@h-<name>`
running `claude --continue` from the agent's state dir. Requires root
(same as all `machinectl shell` operations).
```bash
hivectl choom iris # join iris's ongoing Claude session (--continue)
@ -123,3 +123,11 @@ hivectl choom iris --fresh # start a new Claude session instead
Without `--fresh`, `--continue` is passed so the operator joins the
agent's live context window. With `--fresh` a clean session starts.
The container must be running.
The session is entered **as the agent user**, not root: `machinectl
shell` defaults to root in the container, which would make claude read
`/root/.claude` (empty) instead of the agent's `/home/<name>/.claude`
where its OAuth credentials + settings live. choom prefixes the
machine with `<name>@` (the meta-flake sets the agent's unix user name
to its label) and `cd`s into `/agents/<name>/state` first so
`--continue` resolves the same per-project session the harness runs.

View file

@ -73,11 +73,19 @@ enum Cmd {
},
/// Open an interactive Claude session inside an agent container.
///
/// Replaces the current process with `machinectl shell h-<name>`
/// running `claude --continue` — drops the operator straight into
/// the agent's live Claude session with its full loaded context and
/// persona. Requires root (same as all machinectl shell operations)
/// and the container must be running.
/// Replaces the current process with `machinectl shell
/// <name>@h-<name>` running `claude --continue` from the agent's
/// state dir — drops the operator straight into the agent's live
/// Claude session with its full loaded context and persona. Requires
/// root (same as all machinectl shell operations) and the container
/// must be running.
///
/// The session is entered **as the agent user** (not root): claude
/// reads its OAuth credentials + settings from the agent's
/// `/home/<name>/.claude`, and the working directory is set to the
/// agent's state dir (`/agents/<name>/state`) so `--continue`
/// resumes the same per-project session the harness runs. Entering
/// as root (the `machinectl shell` default) is what loses both.
///
/// Pass `--fresh` to start a new Claude session instead of continuing
/// the most recent one.
@ -357,10 +365,24 @@ fn is_agent(name: &str) -> bool {
/// Drop into an interactive Claude session in the agent container.
///
/// Replaces the current process (exec) with `machinectl shell h-<name>
/// /run/current-system/sw/bin/claude [--continue]`. The `--continue`
/// flag is passed by default so the operator joins the agent's active
/// session; omit it via `--fresh` to start a blank session instead.
/// Replaces the current process (exec) with `machinectl shell
/// <name>@h-<name> /bin/sh -lc 'cd /agents/<name>/state && exec claude
/// [--continue]'`. Two things matter here, both of which the naive
/// `machinectl shell h-<name> claude` got wrong (issue: choom missing
/// creds + settings):
///
/// 1. **Run as the agent user.** `machinectl shell` defaults to root in
/// the container, so claude would read `/root/.claude` (empty)
/// instead of the agent's `/home/<name>/.claude` where the OAuth
/// credentials + settings live. Prefixing the machine with
/// `<name>@` enters the session as the agent user (the meta-flake
/// sets `hyperhive.user.name` to the agent label, so the unix user
/// name matches the agent name).
/// 2. **Start in the agent's state dir.** `claude --continue` resumes
/// the most recent session *for the current project directory*. The
/// harness runs claude from `/agents/<name>/state`, so choom has to
/// `cd` there or `--continue` finds no session (it would look under
/// the user's home instead).
///
/// `machinectl shell` inherits the caller's PTY, so the session is
/// fully interactive. Requires root and a running container.
@ -369,12 +391,25 @@ fn choom(name: &str, fresh: bool) -> Result<()> {
bail!("no such agent: '{name}' (no state dir under /var/lib/hyperhive/agents/)");
}
let container = hive_c0re::lifecycle::container_name(name);
// The agent's unix user name matches its agent name (meta-flake
// sets `hyperhive.user.name = <label>`); enter the session as that
// user so claude sees the right `$HOME/.claude`.
let target = format!("{name}@{container}");
let claude = "/run/current-system/sw/bin/claude";
// In-container state dir (host `/var/lib/hyperhive/agents/<name>/state`
// is bind-mounted here); matches `hive-ag3nt::paths::state_dir()`.
let state_dir = format!("/agents/{name}/state");
let continue_flag = if fresh { "" } else { " --continue" };
// `cd` so `--continue` resolves the harness's per-project session;
// `exec` replaces the shell with claude so it owns the PTY directly.
// `name` is constrained to `[a-z0-9._-]` so there's nothing to quote.
let inner = format!("cd {state_dir} && exec {claude}{continue_flag}");
let mut cmd = std::process::Command::new("machinectl");
cmd.arg("shell").arg(&container).arg(claude);
if !fresh {
cmd.arg("--continue");
}
cmd.arg("shell")
.arg(&target)
.arg("/bin/sh")
.arg("-lc")
.arg(&inner);
// exec() replaces the current process — we inherit stdin/stdout/stderr
// (the caller's PTY) so the Claude session is fully interactive.
// This call only returns on error.