How do I launch Claude Code inside a systemd scope?

Table of Contents

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

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

Emacs 29.3 (Org mode 9.6.15)