[Unit] Description=Overlord Agent (Claude Code shell-out service) After=network-online.target Wants=network-online.target [Service] Type=simple User=erik Group=erik # Working directory MUST be the repo root so: # - claude -p sessions land at ~/.claude/projects/-home-erik-MosswartOverlord/ # - .mcp.json is auto-loaded WorkingDirectory=/home/erik/MosswartOverlord # Secrets moved OUT of /home/erik/ to /etc/overlord/agent.env so # ProtectHome=read-only blocks their read entirely. The file is # root-owned, mode 0640, group=erik. EnvironmentFile=-/etc/overlord/agent.env # Backwards-compat: also try the old location during transition. EnvironmentFile=-/home/erik/MosswartOverlord/.env # Run inside the venv populated by install.sh. ExecStart=/home/erik/MosswartOverlord/agent/.venv/bin/python -m agent.service Restart=on-failure RestartSec=3 StandardOutput=journal StandardError=journal # ─── Resource caps ───────────────────────────────────────────────── MemoryMax=512M CPUQuota=200% TasksMax=128 # ─── Filesystem hardening ────────────────────────────────────────── # /usr, /boot, /efi become read-only; /etc + /var get a writable overlay # that's discarded on stop. Subprocesses inherit these protections. ProtectSystem=strict ProtectHome=read-only # Allow writing only to the explicit paths claude / our service need. # - ~/.claude — session JSONL files # - .venv pycache — minor pip cache writes ReadWritePaths=/home/erik/.claude ReadWritePaths=/home/erik/MosswartOverlord/agent/.venv ReadWritePaths=/var/log/overlord-agent # Keep $HOME visible to the venv python so it can find pip cache etc. # (read-only via ProtectHome=read-only — this writable carve-out is # narrowly the .claude session dir above.) LogsDirectory=overlord-agent LogsDirectoryMode=0755 PrivateTmp=true PrivateDevices=true ProtectClock=true ProtectKernelTunables=true ProtectKernelModules=true ProtectKernelLogs=true ProtectControlGroups=true ProtectHostname=true ProtectProc=invisible ProcSubset=pid # Hide sensitive host paths even if something in the python or claude # subprocess tree tries to read them. InaccessiblePaths=/etc/shadow InaccessiblePaths=/etc/gshadow InaccessiblePaths=/etc/ssh InaccessiblePaths=/root InaccessiblePaths=-/home/erik/.ssh InaccessiblePaths=-/home/erik/.bash_history InaccessiblePaths=-/home/erik/.zsh_history # ─── Privilege & capability hardening ────────────────────────────── NoNewPrivileges=true CapabilityBoundingSet= AmbientCapabilities= LockPersonality=true RestrictRealtime=true RestrictSUIDSGID=true RemoveIPC=true # MemoryDenyWriteExecute would break Node.js (V8 JIT requires W^X # transitions via mprotect with PROT_EXEC on JITted code pages). Claude # Code is a Node app, so omit this. Without JIT we'd lose all model # performance. The other restrictions still prevent shellcode injection # in practice (no Bash/Write tools, no shellcraft surface). # MemoryDenyWriteExecute=true ← DO NOT enable; breaks Node V8 JIT RestrictNamespaces=true # ─── Network family restriction ──────────────────────────────────── # Block raw/packet sockets so even a kernel-LPE-class bug can't sniff # traffic or forge packets. We don't IPAddressAllow-restrict because # Anthropic's Cloudflare IPs shift and the whitelist would break claude. # If you need true egress filtering, run nftables scoped to this # service's cgroup — that's reliable in a way IPAddressAllow isn't. RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 # ─── Syscall filter ──────────────────────────────────────────────── # Use the standard @system-service preset which is what almost every # hardened systemd unit uses. It already excludes the dangerous groups # (privileged, mount, reboot, raw-io, etc.) by NOT including them, while # being broad enough to host typical apps including Node.js. # # We tried adding extra "~@..." negations on top — they killed Claude # (Node) with SIGSYS during startup. The default @system-service preset # is the right balance; the rest of the hardening covers what we need. SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallFilter=~@privileged SystemCallFilter=~@reboot SystemCallFilter=~@mount [Install] WantedBy=multi-user.target