Skip to content

Conversation

@jeanlucthumm
Copy link

@jeanlucthumm jeanlucthumm commented Jan 26, 2026

Summary

Adds a NixOS module (nixosModules.clawdbot) that runs the gateway as an isolated system user with systemd hardening.

Closes #22

Important

Depends on #10 - This PR is based on cherry-picked commits from #10 (NixOS and aarch64-linux support). It should be rebased once #10 is merged.

Security Motivation

Currently the home-manager module runs the gateway as the user's personal account, giving the LLM full access to SSH keys, credentials, personal files, etc. Running as a dedicated locked-down system user contains the blast radius.

Features

  • Dedicated clawdbot system user with minimal privileges
  • System-level systemd service with hardening:
    • ProtectHome=true, ProtectSystem=strict
    • PrivateTmp=true, PrivateDevices=true
    • NoNewPrivileges=true
    • CapabilityBoundingSet="" (no capabilities)
    • SystemCallFilter=@system-service
    • Full namespace/kernel protection
  • Multi-instance support via instances.<name>
  • Credential loading from files at runtime
  • Documents and skills installation support

Usage

services.clawdbot = {
  enable = true;
  providers.anthropic.oauthTokenFile = config.age.secrets.clawdbot-token.path;
  providers.telegram = {
    enable = true;
    botTokenFile = config.age.secrets.telegram-token.path;
    allowFrom = [ 12345678 ];
  };
};

Test plan

  • NixOS VM integration test (nix build .#checks.x86_64-linux.nixos-clawdbot)
  • Deployed and tested on real system

Todos

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit odd that the name of the file on-disk is CLAUDE.md while the name of the file in it's own heading is "AGENTS.md"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just copy pasted it so I can use it with Claude Code.

I haven't seen a good way to have both coexist in a repo, might delete before merge but open to suggestions.

das-monki and others added 29 commits January 28, 2026 16:33
What:
- Remove hardcoded `/bin/` paths from activation scripts
- Use home-manager's `run` function for mkdir and ln commands

How:
- Change `/bin/mkdir -p` and `/bin/ln -sfn` to `run mkdir -p` and
  `run ln -sfn` in clawdbotDirs and clawdbotConfigFiles activation blocks

Why:
- Hardcoded `/bin/mkdir` and `/bin/ln` don't exist on NixOS, where
  binaries live in the Nix store and are accessed via PATH
- Using bare commands (mkdir, ln) works but breaks dry-run mode
- `run` is the home-manager 25.11 convention, replacing the deprecated
  `$DRY_RUN_CMD`. It executes normally but only prints when using
  `home-manager switch --dry-run`
- This matches home-manager's own modules (e.g., xdg-user-dirs.nix)

Fixes: moltbot#5
These attributes were missing from defaultInstanceConfig, causing
evaluation failures when using the simple configuration style.
Paths like ~/.clawdbot need to be expanded to absolute paths for
systemd, which cannot interpret literal ~.
The gateway now expects telegram config under channels.telegram
instead of top-level telegram key.
Upstream renamed messages.queue.byProvider to messages.queue.byChannel.
Had this error before:
[telegram] handler failed: Error: Missing workspace template: AGENTS.md
Automatically update version strings in clawdbot-gateway.nix and check
derivations when running update-pins.sh, keeping them in sync with the
upstream release tag.
Duplicated option definitions from home-manager module, adapted for
NixOS system service:
- Namespace: services.clawdbot (not programs.clawdbot)
- Default paths: /var/lib/clawdbot (not ~/.clawdbot)
- Added: user, group options for system user
- Removed: launchd.*, app.*, appDefaults.* (macOS-specific)
- Skills default to "copy" mode (no user home for symlinks)

Marked with TODO for future consolidation with home-manager options.
Implements the core NixOS module:
- System user/group creation (static, not DynamicUser)
- Systemd service with comprehensive hardening:
  - ProtectHome, ProtectSystem=strict, PrivateTmp
  - Capability restrictions (empty CapabilityBoundingSet)
  - System call filtering (@System-service)
  - Network restrictions (AF_INET/AF_INET6/AF_UNIX only)
  - Namespace restrictions
  - Note: MemoryDenyWriteExecute disabled for Node.js JIT
- Gateway wrapper script for credential loading
- Config generation (mirrored from home-manager patterns)
- tmpfiles rules for state directories
- Config files in /etc/clawdbot/ with symlinks to state dir
Tests:
- Service starts successfully
- User/group created (clawdbot:clawdbot)
- State directories exist with correct ownership
- Config file symlinked to /var/lib/clawdbot
- Hardening: ProtectHome hides /home from service
- Service runs as clawdbot user

Run with: nix build .#checks.x86_64-linux.nixos-module -L
Interactive: nix build .#checks.x86_64-linux.nixos-module.driverInteractive
The /run/agenix and /run/secrets paths were causing service startup
failure when they don't exist. With ProtectSystem=strict, /run/* is
already readable, so these explicit paths are unnecessary.
Adds providers.anthropic.oauthCredentialsDir option that bind-mounts
the user's ~/.claude directory into the service's sandbox. This allows
using Claude Pro/Max subscription via OAuth while maintaining isolation.

The service:
- Still has ProtectHome=true (can't see /home generally)
- Gets read-write access to ONLY the specified .claude dir via BindPaths
- Can refresh OAuth tokens as needed

Example:
  services.clawdbot.providers.anthropic.oauthCredentialsDir = "/home/user/.claude";
Adds second test node (oauth) that verifies:
- Service starts with oauthCredentialsDir configured
- Credentials are accessible via bind-mount at /var/lib/clawdbot/.claude
- Other files in /home remain inaccessible (ProtectHome still works)
The gateway uses os.networkInterfaces() which requires AF_NETLINK
sockets to enumerate interfaces. Without this, the service fails with:
  uv_interface_addresses returned Unknown system error 97
System services aren't exposed externally, so disable gateway auth by
default. Users can override via configOverrides if needed.
Match home-manager behavior - don't set gateway.auth.mode in the module.
Users can configure via configOverrides based on their use case.
Upstream now requires gateway authentication by default (c4a80f4ed).
Add gateway.auth.{mode,tokenFile,passwordFile} options to support this.

Users can either:
- Set tokenFile/passwordFile to load credentials from files at runtime
- Use configOverrides to set gateway.auth.token directly in the config

The wrapper script loads credentials from files and sets the appropriate
environment variables (CLAWDBOT_GATEWAY_TOKEN/CLAWDBOT_GATEWAY_PASSWORD).
The @mariozechner/clipboard native module (transitive dependency from pi-tui)
needs resource syscalls that were blocked by ~@resources filter, causing
SIGSYS crashes on headless systems.
Remove ~@PRIVILEGED and ~@resources filters - Node.js with native modules
needs these syscalls. Security is maintained through:
- CapabilityBoundingSet = "" (no capabilities)
- NoNewPrivileges = true
- ProtectHome, ProtectSystem = strict
- RestrictNamespaces, PrivateDevices, PrivateTmp
Instance-level providers.telegram and providers.anthropic options now
default to the top-level cfg values instead of hardcoded defaults.
This ensures that when users set top-level providers and only override
specific instance options (like configOverrides), the provider settings
are still inherited.
BindPaths can't access /home when ProtectHome=true, so disable it
when oauthCredentialsDir is configured (typically points to ~/.claude).
The short-lived OAuth tokens from Claude CLI's .credentials.json aren't
practical for server deployments. Replace with oauthTokenFile which loads
a long-lived token from `claude setup-token`.

- Remove oauthCredentialsDir option and bind mount logic
- Add oauthTokenFile option (sets ANTHROPIC_OAUTH_TOKEN env var)
- Restore ProtectHome=true now that we don't need /home access
- Update module header with new example usage
The OAuth bind-mount test referenced oauthCredentialsDir which was
removed in ddc7bd5. The new oauthTokenFile approach doesn't need
bind mounts - tokens are loaded via env vars from paths outside /home.
These options were defined but never wired up to the config generation.
Rather than ship dead options, remove them and add a note pointing to
the home-manager module which has the full implementation.
Add documents-skills.nix with parallel implementation to home-manager:
- Documents: copies AGENTS.md, SOUL.md, TOOLS.md to workspace with
  appended Nix-managed tools report
- Skills: supports copy and inline modes (symlink omitted for system
  service where it doesn't make sense)
- Uses systemd-tmpfiles for installation

The implementation is kept separate to ease future consolidation with
the home-manager module.
The `config` option (using generatedConfigOptions from upstream schema)
was defined but never wired into config generation. Users setting
`inst.config.*` would see no effect, which is confusing.

The `configOverrides` option remains as the escape hatch for arbitrary
JSON config. If typed schema options are needed later, they can be
added with actual implementation.
Fail early with clear messages when:
- Neither apiKeyFile nor oauthTokenFile is set for Anthropic
- gateway.auth.tokenFile is missing when mode is "token"
- gateway.auth.passwordFile is missing when mode is "password"

Update test to provide dummy tokens to satisfy assertions.
Both NixOS and home-manager now default to claude-sonnet-4 which is
a sensible default (faster, cheaper). Users wanting opus can override.
The instances option was defined in both options.nix and clawdbot.nix.
The one in clawdbot.nix always overrode the one in options.nix, making
the latter dead code.
Rename all NixOS module files and references:
- clawdbot.nix → moltbot.nix
- services.clawdbot → services.moltbot
- /var/lib/clawdbot → /var/lib/moltbot
- user/group default: clawdbot → moltbot
- Add MOLTBOT_* env vars as primary, keep CLAWDBOT_*/CLAWDIS_* for compat
Update flake.nix and NixOS module integration test to use moltbot naming.
Update PR.md and CLAUDE.md with new repo URLs and naming.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add NixOS module for running gateway as isolated system user

3 participants