feat(#1969): ship claude settings via /etc/claude-code/managed-settings.json

This commit is contained in:
damocles 2026-06-26 21:36:16 +02:00 committed by mara
commit b231ed2392
6 changed files with 49 additions and 65 deletions

View file

@ -9,7 +9,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Context, Result, bail};
use anyhow::{Result, bail};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
@ -17,12 +17,13 @@ use crate::events::{Bus, LiveEvent};
use crate::login::LoginState;
use crate::mcp;
// `--settings` JSON is read at runtime from
// `$HIVE_ASSETS_DIR/prompts/claude-settings.json` via
// `hive_sh4re::assets::claude_settings()`. We turn off claude's
// in-session auto-compaction and its cross-session auto-memory because
// hyperhive owns those concerns (`/compact` on overflow, notes
// persistence under `/state`). Unknown keys are silently ignored by
// Hive-enforced claude settings ship at `/etc/claude-code/managed-settings.json`
// (wired in `nix/templates/harness-base.nix` from the `prompts/claude-settings.json`
// asset). claude-code auto-discovers that managed path — precedence #1,
// read-only, un-overridable — so the harness no longer passes `--settings`.
// We turn off claude's in-session auto-compaction and its cross-session
// auto-memory because hyperhive owns those concerns (`/compact` on overflow,
// notes persistence under `/state`). Unknown keys are silently ignored by
// claude-code; if a key gets renamed we'll spot it because the
// corresponding behavior will start firing mid-turn again.
@ -95,8 +96,10 @@ conversation to go on. Do not start new work or reply to anyone — just write y
and end the turn.";
/// The set of files claude reads on every invocation: the MCP server
/// config (`--mcp-config`), static settings (`--settings`), and the
/// pre-rendered role/tools system prompt (`--system-prompt-file`).
/// config (`--mcp-config`) and the pre-rendered role/tools system
/// prompt (`--system-prompt-file`). Static settings are no longer
/// passed here — they live at `/etc/claude-code/managed-settings.json`
/// and claude auto-discovers them.
/// Materialised once at harness startup; shared between the turn loop
/// and the operator-driven `/compact` path so both invocations look
/// identical to claude (same MCP surface, same allowed tools, same
@ -104,7 +107,6 @@ and end the turn.";
#[derive(Clone)]
pub struct TurnFiles {
pub mcp_config: PathBuf,
pub settings: PathBuf,
pub system_prompt: PathBuf,
}
@ -118,7 +120,6 @@ impl TurnFiles {
pub async fn prepare(socket: &Path, label: &str) -> Result<Self> {
Ok(Self {
mcp_config: write_mcp_config(socket).await?,
settings: write_settings(socket).await?,
system_prompt: write_system_prompt(socket, label).await?,
})
}
@ -145,33 +146,6 @@ pub async fn write_mcp_config(socket: &Path) -> Result<PathBuf> {
Ok(path)
}
/// Drop the static `--settings` JSON next to the MCP config so we can
/// pass a path (`--settings <file>`) instead of an ever-growing inline
/// blob — the CLI argv has a finite length budget.
///
/// # Errors
///
/// Returns an error if the settings file cannot be written.
pub async fn write_settings(_socket: &Path) -> Result<PathBuf> {
let parent = crate::paths::config_dir();
tokio::fs::create_dir_all(&parent).await.ok();
let path = parent.join("claude-settings.json");
// Source-of-truth is `$HIVE_ASSETS_DIR/prompts/claude-settings.json`;
// copy through the per-agent runtime dir so claude reads it from the
// same socket-adjacent location every time and so a future override
// (per-agent settings JSON layer) drops in cleanly.
let src = hive_sh4re::assets::claude_settings();
tokio::fs::copy(&src, &path).await.with_context(|| {
format!(
"copy claude settings from {} to {}",
src.display(),
path.display()
)
})?;
tracing::info!(path = %path.display(), "wrote claude settings");
Ok(path)
}
/// Thin re-export of [`crate::prompt::write_system_prompt`] for
/// callers that already import this module. The actual rendering +
/// marker-block logic lives in `prompt.rs`; this is just the public
@ -579,8 +553,9 @@ fn session_refreshed(prev: DirSnapshot, now: DirSnapshot) -> bool {
/// live event bus. Prompt goes over stdin (variadic
/// `--allowedTools`/`--tools` would otherwise eat a trailing positional
/// prompt). The session is persistent across turns via `--continue` and
/// claude's in-session auto-compact is disabled via `--settings` so it
/// doesn't stall mid-turn — hyperhive owns compaction.
/// claude's in-session auto-compact is disabled via the managed
/// settings at `/etc/claude-code/managed-settings.json` so it doesn't
/// stall mid-turn — hyperhive owns compaction.
pub async fn run_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutcome {
match run_claude(prompt, files, bus).await {
Ok((true, _, _)) => TurnOutcome::PromptTooLong,
@ -675,9 +650,7 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result<(bool,
.arg("--model")
.arg(&model)
.arg("--effort")
.arg(&effort)
.arg("--settings")
.arg(&files.settings);
.arg(&effort);
if resume {
cmd.arg("--continue");
}

View file

@ -51,9 +51,9 @@ struct AppState {
bus: Bus,
socket: PathBuf,
/// Same `TurnFiles` the harness's turn loop uses. Shared so
/// `/api/compact` re-uses the exact MCP config / system prompt /
/// settings claude saw on the last regular turn — keeps the
/// session shape identical across compact + normal turns.
/// `/api/compact` re-uses the exact MCP config / system prompt
/// claude saw on the last regular turn — keeps the session shape
/// identical across compact + normal turns.
files: TurnFiles,
/// Prevents `/api/compact` from racing with an in-flight normal turn.
turn_lock: TurnLock,