RustifyMyClaw resolves the config path using this priority chain:
| Priority | Source | Example |
|---|---|---|
| 1 | -f / --config-file CLI flag |
rustifymyclaw -f ./my-config.yaml |
| 2 | RUSTIFYMYCLAW_CONFIG environment variable |
export RUSTIFYMYCLAW_CONFIG=~/projects/config.yaml |
| 3 | ./config.yaml in the current working directory |
cd my-project && rustifymyclaw |
| 4 | ~/.rustifymyclaw/config.yaml |
Per-user config (Unix) or %APPDATA%\RustifyMyClaw\config.yaml (Windows) |
| 5 | /etc/rustifymyclaw/config.yaml |
System-wide config (Unix only) |
The first existing file wins. Use rustifymyclaw config path to see which path would be used from your current directory.
workspaces:
- name: "my-project" # unique name, used by /use command
directory: "/home/user/projects/my-project" # must exist at startup
backend: "claude-cli" # claude-cli | codex-cli | gemini-cli
timeout_seconds: 300 # optional — kill CLI process after N seconds
channels:
- kind: telegram
bot_name: "@mybot" # optional — display/logging only
token: "${TELEGRAM_BOT_TOKEN}" # bot token from @BotFather
allowed_users:
- 123456789 # numeric Telegram user ID
- "@username" # or handle
max_message_chars: 3500 # optional — overrides global output.max_message_chars
- kind: whatsapp
token: "${WHATSAPP_API_TOKEN}" # Meta Graph API token
phone_number_id: "${WA_PHONE_NUMBER_ID}" # Meta Business phone number ID (required)
webhook_port: 8080 # optional — inbound webhook port (default 8080)
verify_token: "${WA_VERIFY_TOKEN}" # optional — webhook verification token
allowed_users:
- "+15551234567" # phone number in E.164 format
- kind: slack
token: "${SLACK_BOT_TOKEN}" # xoxb-* bot token
app_token: "${SLACK_APP_TOKEN}" # xapp-* Socket Mode token (required)
use_threads: true # optional — reply in-thread (default false)
max_message_chars: 3000 # optional — Slack renders poorly above ~3000
allowed_users:
- "@dev_user" # Slack handle
- "U01ABC123" # or raw Slack user ID
- name: "data-pipeline"
directory: "/home/user/projects/pipeline"
backend: "codex-cli"
channels:
- kind: telegram
token: "${PIPELINE_BOT_TOKEN}"
allowed_users:
- "@teammate"
output:
max_message_chars: 4000 # default max chars per message chunk
file_upload_threshold_bytes: 51200 # 50 KB — responses larger than this are sent as a file
chunk_strategy: "natural" # natural | fixed
# Optional — absent means no rate limiting
limits:
max_requests: 10
window_seconds: 60| Field | Type | Required | Description |
|---|---|---|---|
workspaces |
list | yes | At least one workspace required. |
output |
object | yes | Global output settings. |
limits |
object | no | Rate limiting policy. Absent = no limit. |
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | — | Unique workspace name. Used by /use command. |
directory |
path | yes | — | Working directory for CLI invocations. Must exist at startup. |
backend |
string | yes | — | claude-cli, codex-cli, or gemini-cli. |
channels |
list | yes | — | At least one channel required. |
timeout_seconds |
integer | no | none | Kill the CLI process after this many seconds. Absent = no timeout. |
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
kind |
string | yes | — | telegram, whatsapp, or slack. |
token |
string | yes | — | Platform bot token. Supports ${ENV_VAR}. |
allowed_users |
list | yes | — | Non-empty. Numeric IDs or string handles. |
bot_name |
string | no | — | Display name for logs only. |
max_message_chars |
integer | no | global | Override output.max_message_chars for this channel. |
file_upload_threshold_bytes |
integer | no | global | Override output.file_upload_threshold_bytes for this channel. |
WhatsApp-only fields (ignored with a warning on other channel kinds):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
phone_number_id |
string | yes | — | Meta Business phone number ID. |
webhook_port |
integer | no | 8080 | Port for the inbound webhook HTTP server. |
verify_token |
string | no | — | Webhook verification token sent by Meta. |
Slack-only fields (ignored with a warning on other channel kinds):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
app_token |
string | yes | — | xapp-* Socket Mode token. |
use_threads |
bool | no | false | Send responses as thread replies. |
| Field | Type | Required | Description |
|---|---|---|---|
max_message_chars |
integer | yes | Maximum characters per outbound message chunk. |
file_upload_threshold_bytes |
integer | yes | If total response exceeds this size, send as a file instead of chunking. |
chunk_strategy |
string | yes | natural (boundary-aware) or fixed (hard cut). |
| Field | Type | Required | Description |
|---|---|---|---|
max_requests |
integer | yes | Maximum messages per user within the sliding window. |
window_seconds |
integer | yes | Sliding window width in seconds. |
Any config value can reference an environment variable with ${VAR_NAME}:
token: "${TELEGRAM_BOT_TOKEN}"The config loader replaces all ${...} patterns at parse time using std::env::var. If a referenced variable is not set, RustifyMyClaw exits with an error at startup — no silent failures.
Never hardcode tokens directly in config.yaml. Use env var interpolation and keep your shell environment or a secrets manager as the source of truth.
max_message_chars and file_upload_threshold_bytes can be set per channel to override the global output defaults. The merge rule:
- If the channel sets a value → use the channel value.
- If the channel omits a value → use the global
outputvalue. chunk_strategyis always global — there is no per-channel override.
This is handled by effective_output_config() in src/config.rs and applied at startup. Each channel stamps its resolved config onto every InboundMessage it produces.
claude-cli — Invokes claude -p "<prompt>". When a session is active, adds the -c flag to continue the conversation. Session state is managed by SessionStore; /new resets it.
codex-cli — Invokes codex -q "<prompt>". No session continuation flag — each invocation is independent.
gemini-cli — Invokes gemini -p "<prompt>" -y. No session continuation. The -y flag suppresses confirmation prompts.
RustifyMyClaw validates the config at startup and exits with a descriptive error if any of the following are true:
workspacesis empty.- A workspace
nameis an empty string. - A workspace
directorydoes not exist on disk. - A workspace
backendis not one ofclaude-cli,codex-cli,gemini-cli. - A workspace has no
channels. - A channel
kindis not one oftelegram,whatsapp,slack. - A channel
allowed_userslist is empty.
If a platform-specific field appears on the wrong channel kind (e.g. phone_number_id on a Telegram channel), a warning is logged at startup and the field is ignored. The process does not exit.
RustifyMyClaw watches config.yaml for changes using the notify crate. Changes are debounced (300ms). On a valid reload:
| Field(s) | Behavior |
|---|---|
limits |
Applied immediately — the in-memory rate limiter is updated. |
output |
Logged as changed. Requires restart to apply. |
| Workspaces added/removed | Logged as changed. Requires restart to apply. |
| Backend changed | Logged as changed. Requires restart to apply. |
| Channel tokens | Logged as changed. Requires restart to apply. |
allowed_users |
Logged as changed. Requires restart to apply. |
Invalid configs (YAML errors, missing env vars, validation failures) are logged and the running config remains in effect.
sudo bash scripts/install.sh --systemThis installs the binary to /usr/local/bin/, creates the rustifymyclaw system user and group, sets up /etc/rustifymyclaw/ with a starter config, env file, and systemd unit.
- Copy the binary to
/usr/local/bin/and the unit file:
sudo cp rustifymyclaw /usr/local/bin/
sudo chmod 755 /usr/local/bin/rustifymyclaw
sudo cp systemd/rustifymyclaw.service /etc/systemd/system/
sudo systemctl daemon-reload- Create the service user and config directory:
sudo groupadd -r rustifymyclaw
sudo useradd -r -g rustifymyclaw -s /usr/sbin/nologin -d /nonexistent rustifymyclaw
sudo mkdir -p /etc/rustifymyclaw
sudo cp examples/config.yaml /etc/rustifymyclaw/config.yaml
sudo cp systemd/env.example /etc/rustifymyclaw/env
sudo chown root:rustifymyclaw /etc/rustifymyclaw
sudo chmod 750 /etc/rustifymyclaw
sudo chown root:rustifymyclaw /etc/rustifymyclaw/config.yaml
sudo chmod 640 /etc/rustifymyclaw/config.yaml
sudo chown root:rustifymyclaw /etc/rustifymyclaw/env
sudo chmod 640 /etc/rustifymyclaw/env- Edit
config.yamlwith your workspaces and channels.
sudo nano /etc/rustifymyclaw/config.yaml- Add API tokens to
env:
sudo nano /etc/rustifymyclaw/env- Enable and start:
sudo systemctl enable --now rustifymyclawIf you wish to use a configuration file other than the default /etc/rustifymyclaw/config.yaml, you can override it using the RUSTIFYMYCLAW_CONFIG environment variable. Ex. use the /etc/rustifymyclaw/env file:
# In /etc/rustifymyclaw/env
RUSTIFYMYCLAW_CONFIG=/path/to/your/custom-config.yamlImportant: The daemon runs as the rustifymyclaw system user. Any custom configuration file must be readable by this user (e.g., chown root:rustifymyclaw and chmod 640).
The daemon runs as the dedicated rustifymyclaw system user. Workspace directories are read-only by default. Without this step your CLI agent can still read the project and answer questions about it, but cannot write or edit files. If you expect your agent to have write permissions, you must explicitly allow each workspace path.
Use the built-in command:
sudo rustifymyclaw config allow-path /home/user/projects/my-projectThis command does three things:
- Grants traverse (
x) ACLs on each parent directory so the daemon can reach the workspace (e.g.,/home/user,/home/user/projects). - Grants recursive read/write ACLs on the workspace directory and all contents, with default ACLs so newly created files inherit permissions.
- Adds a
ReadWritePaths=entry to the systemd override so the sandbox allows writes.
Then reload: sudo systemctl daemon-reload && sudo systemctl restart rustifymyclaw
Prerequisites: The acl package must be installed (sudo apt install acl on Debian/Ubuntu, sudo dnf install acl on Fedora/RHEL).
To revoke access manually, remove the ACLs and the systemd override entry:
sudo setfacl -R -x u:rustifymyclaw /home/user/projects/my-project
sudo setfacl -R -d -x u:rustifymyclaw /home/user/projects/my-project
# Then edit /etc/systemd/system/rustifymyclaw.service.d/override.conf
# and remove the corresponding ReadWritePaths= line.
sudo systemctl daemon-reload && sudo systemctl restart rustifymyclawNote on default ACLs: When allow-path sets default ACLs on a workspace directory, newly created files inherit permissions for the rustifymyclaw user. This changes how umask interacts with file creation in that directory. The ACL mask, not the process umask, determines effective permissions for new files.
The included systemd unit uses:
User=rustifymyclaw/Group=rustifymyclaw— dedicated static system user with no login shell and no home directory.NoNewPrivileges=yes— the process cannot gain new privileges.ProtectSystem=strict— the filesystem is read-only except for allowed paths.ProtectHome=false— home directories are accessible (workspace traversal requires this; actual access is gated by POSIX ACLs).ReadOnlyPaths=/— everything read-only by default; individual workspaces are granted write access viaallow-path.PrivateTmp=yes— isolated/tmpnamespace.EnvironmentFile— secrets loaded from/etc/rustifymyclaw/env(mode 640, owned byroot:rustifymyclaw).
Versions prior to this release used DynamicUser=yes which allocated an ephemeral UID. If you are upgrading from an earlier version:
- The service now runs as the static
rustifymyclawuser instead of a dynamic UID. - Any manual ACLs you previously granted to the dynamic UID are orphaned — re-apply them with
sudo rustifymyclaw config allow-path <path>. - The installer (
scripts/install.sh --system) creates the user automatically. For manual upgrades, create the user first:sudo groupadd -r rustifymyclaw sudo useradd -r -g rustifymyclaw -s /usr/sbin/nologin -d /nonexistent rustifymyclaw
- Update ownership on config files:
sudo chown root:rustifymyclaw /etc/rustifymyclaw /etc/rustifymyclaw/config.yaml /etc/rustifymyclaw/env sudo chmod 750 /etc/rustifymyclaw sudo chmod 640 /etc/rustifymyclaw/config.yaml /etc/rustifymyclaw/env