From e553e40577077b8a7bf50f111ab941295aaf4ffc Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 4 Jun 2026 14:48:08 +0200 Subject: [PATCH 1/2] fix: soft-fail chmod in ensure_claude_dir when dir is agent-owned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hive-agent-user-migrate chowns the bind-mounted claude dir to the agent user on every container boot. After that, hive-core (a different user) cannot chmod it — set_permissions fails with EPERM, which was propagated as an error and caused the rebuild to fail entirely. Fix: make the chmod best-effort. Newly created dirs (owned by hive-core) get the 0755 mode set immediately; after the agent-migration chown the mode is preserved so claude_has_session works correctly. Subsequent calls that hit the EPERM path just log at DEBUG and continue. --- hive-c0re/src/lifecycle.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index dd635bf8..72821fca 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -781,11 +781,23 @@ pub fn ensure_claude_dir(claude_dir: &Path) -> Result<()> { // list the directory so `claude_has_session` can detect a valid session. // The credential files inside (`.credentials.json` etc.) are 0600 so the // secrets themselves stay private regardless of the directory mode. + // + // Best-effort: on the first container boot, `hive-agent-user-migrate` + // chowns this dir to the agent user. After that, hive-core (a different + // user) cannot chmod it — that's fine because the mode set during + // initial creation (0755) is preserved through the chown. #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(claude_dir, std::fs::Permissions::from_mode(0o755)) - .with_context(|| format!("chmod 755 {}", claude_dir.display()))?; + if let Err(e) = + std::fs::set_permissions(claude_dir, std::fs::Permissions::from_mode(0o755)) + { + tracing::debug!( + path = %claude_dir.display(), + error = %e, + "ensure_claude_dir: chmod 755 skipped (dir likely owned by agent user after migration)" + ); + } } Ok(()) } From 7e229889a6937240ad0434f89363676102b254e3 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 4 Jun 2026 15:03:12 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20argus=20review=20on=20ensu?= =?UTF-8?q?re=5Fclaude=5Fdir=20=E2=80=94=20narrow=20to=20EPERM,=20fix=20do?= =?UTF-8?q?c=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hive-c0re/src/lifecycle.rs | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 72821fca..0797409a 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -767,12 +767,15 @@ pub async fn setup_applied( Ok(()) } -/// Create the per-agent Claude credentials dir if missing. Mode 0700 — only -/// root inside the container reads/writes it. Idempotent: existing dirs are -/// left untouched (an agent's OAuth tokens survive `destroy`/recreate). +/// Create the per-agent Claude credentials dir if missing. Mode 0755 — hive-core +/// needs read+execute to list the directory so `claude_has_session` can detect a +/// valid session; credential files inside (`.credentials.json` etc.) are 0600 so +/// secrets stay private regardless of the directory mode. Idempotent: existing +/// dirs are left untouched (an agent's OAuth tokens survive `destroy`/recreate). /// Public for the `InitConfig` approval path in `actions.rs` which seeds /// dirs without calling the full `spawn`. pub fn ensure_claude_dir(claude_dir: &Path) -> Result<()> { + use std::io; if !claude_dir.exists() { std::fs::create_dir_all(claude_dir) .with_context(|| format!("create {}", claude_dir.display()))?; @@ -784,19 +787,23 @@ pub fn ensure_claude_dir(claude_dir: &Path) -> Result<()> { // // Best-effort: on the first container boot, `hive-agent-user-migrate` // chowns this dir to the agent user. After that, hive-core (a different - // user) cannot chmod it — that's fine because the mode set during - // initial creation (0755) is preserved through the chown. + // user) cannot chmod it (EPERM) — that's fine because the mode set during + // initial creation (0755) is preserved through the chown. Any other error + // (ENOENT, I/O error) is unexpected and propagated. #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - if let Err(e) = - std::fs::set_permissions(claude_dir, std::fs::Permissions::from_mode(0o755)) - { - tracing::debug!( - path = %claude_dir.display(), - error = %e, - "ensure_claude_dir: chmod 755 skipped (dir likely owned by agent user after migration)" - ); + match std::fs::set_permissions(claude_dir, std::fs::Permissions::from_mode(0o755)) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { + tracing::debug!( + path = %claude_dir.display(), + "ensure_claude_dir: chmod 755 skipped (dir likely owned by agent user after migration)" + ); + } + Err(e) => { + return Err(e).with_context(|| format!("chmod 755 {}", claude_dir.display())); + } } } Ok(())