[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 ───────────────────────────────────────────────── MemoryLimit=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=true 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 ──────────────────────────────────────────────── SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallFilter=~@privileged SystemCallFilter=~@resources SystemCallFilter=~@debug SystemCallFilter=~@mount SystemCallFilter=~@cpu-emulation SystemCallFilter=~@obsolete SystemCallFilter=~@reboot SystemCallFilter=~@swap SystemCallFilter=~@raw-io [Install] WantedBy=multi-user.target