How do I launch Claude Code inside a systemd scope?
compass claude wraps the claude binary so an interactive session
never inherits its parent's cgroup — no manual systemd-run flags,
no per-machine install step, since every checkout already carries
compass.sh and the slice unit source.
Question
How do I launch Claude Code so systemd-oomd can kill a runaway session without taking down the process that spawned it (e.g. Emacs), and so a memory-hungry session can't take the whole machine down with it?
Answer
./compass.sh claude
All arguments are forwarded verbatim, so this is a drop-in
replacement for the claude binary:
./compass.sh claude -p "some prompt"
On first use it deploys app-claude.slice and the
app-claude-.slice.d/50-limits.conf drop-in to
~/.config/systemd/user/ (re-syncing either if the checked-in
source changes) and runs a daemon-reload, then launches Claude as a
transient systemd-run --user --scope under
app-claude-<env>.slice (<env> from ORES_ENV_NAME in the
checkout's .env). Every session gets its own scope
(claude-<env>-<pid>), so it is a sibling of emacs.service under
app.slice rather than a child — oomd evaluates it as its own kill
candidate instead of taking out the whole parent unit.
The per-environment slice is what actually carries the limits: the
dash-truncated drop-in (app-claude-.slice.d/) applies to every
app-claude-<env>.slice systemd creates on demand, with no
per-environment unit file needed. It sets MemoryHigh=6G
(soft — reclaim/throttle) and MemoryMax=9G=/=MemorySwapMax=2G
(hard — cgroup-local OOM), plus CPUWeight=100 (proportional, so an
idle machine's -j3 build bursts are unaffected). This is the
containment half of the design; app-claude.slice itself carries no
caps, only accounting — see
the resource-management plan (WS-1) for the full rationale and
why isolation (which slice) and containment (a memory ceiling) are
two separate concerns.
When there is no usable user systemd manager (e.g. over ssh without
lingering, some cron/container setups), it falls back to an
unscoped exec rather than failing outright.
This is opt-in and changes nothing for anyone invoking the regular
claude binary directly.
Inspect live accounting with:
systemd-cgtop --user systemctl --user show app-claude-<env>.slice -p MemoryMax -p CPUWeight
Script
compass claude in
projects/ores.compass/src/compass_claude.py,
dispatched from
projects/ores.compass/src/compass.py.
The slice unit source is
projects/ores.compass/src/systemd/app-claude.slice; the
per-environment limits drop-in is
projects/ores.compass/src/systemd/app-claude-.slice.d/50-limits.conf.
Tested by
Manual: compass claude --version deploys/refreshes the slice unit
and drop-in and execs claude --version inside a
claude-<env>-<pid>.scope confirmed active via systemctl --user
list-units, with systemctl --user show
app-claude-<env>.slice -p MemoryMax -p CPUWeight confirming the
drop-in's limits apply; re-running is a no-op when both files are
unchanged (content compared per-file, no redundant daemon-reload).
See also
- Plan: systemd resource management and per-environment isolation — the design doc this recipe implements (WS-1).