Skip to content
Merged
16 changes: 11 additions & 5 deletions claude-mem/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,17 @@ setup:
- |
set -e
exec > /tmp/claude-mem-reconcile.log 2>&1
# Run unconditionally (including on early failure below) so root
# never leaves /home/agent/.claude un-chowned.
trap 'chown -R agent:agent /home/agent/.claude' EXIT
# Runs on every exit, including early failures, so root never
# leaves /home/agent/.claude or settings.json root-owned.
# Enumerated, not recursive: ~/.claude holds runtime-managed
# content this kit doesn't own, so a recursive chown would take
# ownership of paths outside the kit's control. Paths are
# existence-checked because under `set -e` a chown that fails on
# a path this script never created would replace the script's
# exit status from inside the trap, turning a tolerated early
# failure into a failed install.
S=/home/agent/.claude/settings.json
trap 'for p in /home/agent/.claude "$S"; do if [ -e "$p" ]; then chown agent:agent "$p"; fi; done' EXIT
mkdir -p /home/agent/.claude
# Wait for the platform's own settings write to land first — at
# create time the engine seeds this file late in the sequence and
Expand Down Expand Up @@ -135,7 +142,6 @@ setup:
console.log("reconciled");
' "$S"
# root + chown-on-exit: /home/agent/.claude ownership isn't guaranteed
# agent-owned on every base image (matches claude-sbx-statusline,
# claude-ollama).
# agent-owned on every base image.
user: "0"
description: Reconcile settings.json — ensure platform keys and claude-mem's enabledPlugins both present (ordering-immune, idempotent)
5 changes: 4 additions & 1 deletion claude-ollama/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ setup:
}
" > /home/agent/.claude/settings.json
fi
chown -R agent:agent /home/agent/.claude
# Enumerated, not recursive: ~/.claude holds runtime-managed content
# this kit doesn't own, so `chown -R` on the parent would take
# ownership of paths outside the kit's control.
chown agent:agent /home/agent/.claude /home/agent/.claude/settings.json
user: root
description: Seed Claude settings.json (none path; no credential service)
files:
Expand Down
10 changes: 7 additions & 3 deletions spec/SPEC-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -895,9 +895,13 @@ Conforming runtimes provide:
- Install entries running as root MAY write to `/usr/local/bin`, `/opt`,
`/etc`, and `/tmp`.
- `/home/agent` and the workspace belong to the agent user. A root install
step that writes there MUST restore ownership (for example
`chown -R agent:agent /home/agent/.claude`), or later writes by the agent
user fail.
step that writes there MUST restore ownership (enumerate the paths it
touched, for example `chown agent:agent /home/agent/.claude
/home/agent/.claude/settings.json`, rather than recursing over the
parent — `~/.claude` holds runtime-managed content that the kit does not
own, so a kit SHOULD NOT take ownership of the whole directory or couple
itself to whatever the runtime places there), or later writes by the
agent user fail.
- Startup entries and the entrypoint run as the agent user by default and
MUST NOT assume root write access.

Expand Down
12 changes: 9 additions & 3 deletions spec/kit_settings_lift_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,23 @@ func findSettingsInstall(t *testing.T, c *CommandsPolicy) InstallCommand {

// runSettingsInstallScript executes the kit's settings-seeding install script
// in a temp dir, rewriting the absolute /home/agent paths to the temp dir and
// making chown non-fatal (the test process is not root), then returns the
// content of the produced settings.json.
// making both the recursive and enumerated chown forms non-fatal (the test
// process is not root), then returns the content of the produced
// settings.json.
func runSettingsInstallScript(t *testing.T, script, modeEnv string) string {
t.Helper()
tmp := t.TempDir()

// /home/agent -> tmp so the script writes inside the sandbox of the test.
script = strings.ReplaceAll(script, "/home/agent", tmp)
// chown will fail for a non-root test process; keep it non-fatal so the
// `set -e` script does not abort before/after writing the file.
// `set -e` script does not abort before/after writing the file. Two
// patterns: kits still on the recursive form, and kits enumerating
// specific paths. The two ReplaceAll calls can't double-substitute each
// other: "chown -R agent:agent" doesn't contain "chown agent:agent" as a
// substring, since "-R " sits between "chown" and "agent:agent".
script = strings.ReplaceAll(script, "chown -R agent:agent", "chown -R agent:agent 2>/dev/null || true #")
script = strings.ReplaceAll(script, "chown agent:agent", "chown agent:agent 2>/dev/null || true #")

cmd := exec.Command("sh", "-c", script)
cmd.Env = append(os.Environ(), modeEnv)
Expand Down
Loading