This guide walks through deploying all three components: the approval server, the VPS client, and the n8n email workflow.
| Component | Requirement |
|---|---|
| Approval server | Node.js >= 18, npm |
| VPS client | Python >= 3.8, pip |
| Email notifications | n8n instance (self-hosted or cloud) |
| Networking | The approval server must be reachable from both the VPS and from email link clicks (i.e., publicly accessible or via tunnel) |
The approval server is an Express.js application that manages approval requests and serves the approve/deny token pages.
git clone https://github.com/DatafyingTech/safe-rm.git
cd safe-rmcd server
npm installcp ../examples/.env.example .envEdit .env with your values:
PORT=3000
HOST=127.0.0.1
BASE_URL=https://safe-rm.example.com
TRUST_PROXY=true
# Generate a strong secret (shared with the client)
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
SAFE_RM_SECRET=your-generated-secret-here
DATA_DIR=./data
APPROVAL_TIMEOUT_SECONDS=600
CLAUDE_EVENT_TIMEOUT_SECONDS=600
RATE_LIMIT_RPM=100
# n8n webhook URLs (set after importing workflows)
N8N_DELETE_APPROVAL_WEBHOOK=https://your-n8n.example.com/webhook/delete-approval
N8N_CLAUDE_NOTIFICATION_WEBHOOK=https://your-n8n.example.com/webhook/claude-hookKey points:
BASE_URLmust be the public URL where users will click approve/deny links from their email.SAFE_RM_SECRETmust be identical on the server and client. Generate it with:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"TRUST_PROXYshould betrueif running behind nginx or another reverse proxy.
# Production
node src/index.js
# Development (auto-restart on changes)
npm run devThe server listens on 127.0.0.1:3000 by default. Use nginx or Caddy to terminate TLS and proxy to it. An example nginx configuration is provided at examples/nginx.example.conf.
sudo cp ../examples/nginx.example.conf /etc/nginx/sites-available/safe-rm
# Edit the file: set your domain and certificate paths
sudo ln -s /etc/nginx/sites-available/safe-rm /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxCreate /etc/systemd/system/safe-rm-server.service:
[Unit]
Description=safe-rm Approval Server
After=network.target
[Service]
Type=simple
User=saferm
WorkingDirectory=/opt/safe-rm/server
ExecStart=/usr/bin/node src/index.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now safe-rm-serverThe client is a Python script that replaces rm on the VPS where your AI agent runs.
# Copy the client directory to the target VPS, then:
cd client
sudo ./install.shThe installer will:
- Copy
safe-rmto/usr/local/bin/safe-rm - Install the
requestsPython library if missing - Create a symlink
/usr/local/bin/rm->/usr/local/bin/safe-rm - Prompt for your approval server URL and shared secret
- Write the config to
/etc/safe-rm.conf(mode 600) - Create
/etc/profile.d/safe-rm.shto ensure/usr/local/binis first in$PATH
If you prefer to install manually:
# Copy the script
sudo cp client/safe-rm /usr/local/bin/safe-rm
sudo chmod +x /usr/local/bin/safe-rm
# Install Python dependency
pip3 install requests>=2.28.0
# Create symlink so 'rm' resolves to safe-rm
sudo ln -sf /usr/local/bin/safe-rm /usr/local/bin/rm
# Create config
sudo cp client/safe-rm.example.conf /etc/safe-rm.conf
sudo chmod 600 /etc/safe-rm.conf
# Edit /etc/safe-rm.conf with your API URL and secret
# Ensure /usr/local/bin is first in PATH
echo 'export PATH="/usr/local/bin:$PATH"' | sudo tee /etc/profile.d/safe-rm.sh
source /etc/profile.d/safe-rm.sh# Check that 'rm' resolves to safe-rm
which rm
# Should output: /usr/local/bin/rm
# Test with dry-run (no API call, no actual deletion)
rm --dry-run -rf /var/www
# Should show: "RISKY DELETION DETECTED" and "dry-run: would request approval"n8n handles sending the approval emails. You need a running n8n instance.
- Open your n8n instance.
- Go to Workflows > Add Workflow > Import from File.
- Choose one of the templates from
n8n-workflows/:
| File | Use when... |
|---|---|
delete-approval-smtp.json |
You have any SMTP email server |
delete-approval-gmail.json |
You want to use Gmail via OAuth |
claude-hook-notification.json |
You want Claude Code hook notifications |
SMTP:
- In n8n, go to Settings > Credentials > Add Credential.
- Search for SMTP and enter your server details (host, port, user, password, SSL/TLS).
- In the workflow, click the email send node and select your SMTP credential.
Gmail OAuth:
- In n8n, go to Settings > Credentials > Add Credential.
- Search for Gmail OAuth2 and follow the OAuth authorization flow.
- In the workflow, click the email send node and select your Gmail credential.
- SMTP workflows: The recipient defaults to the
ADMIN_EMAILn8n environment variable. Set it in your n8n environment, or edit the email node'ssendTofield directly. - Gmail workflow: Edit the email node and replace
admin@example.comwith your address.
- Toggle the workflow to Active.
- Copy the webhook URL from the Webhook Trigger node (e.g.,
https://your-n8n.example.com/webhook/delete-approval). - Paste it into your approval server's
.envasN8N_DELETE_APPROVAL_WEBHOOK(orN8N_CLAUDE_NOTIFICATION_WEBHOOKfor the Claude hook workflow). - Restart the approval server.
Once all three components are running:
# 1. SSH into your VPS (or set SAFE_RM_ACTIVE=1 to force guarded mode)
export SAFE_RM_ACTIVE=1
# 2. Run a risky command in dry-run mode first
rm --dry-run -rf /var/www/html
# 3. If dry-run output looks correct, run for real
rm -rf /var/www/html
# You should see "RISKY DELETION DETECTED" and "APPROVAL REQUESTED"
# 4. Check your email -- click Approve or Deny
# 5. The terminal should print "APPROVED - proceeding" or "DENIED - deletion blocked"curl https://safe-rm.example.com/health
# Expected: {"status":"ok","uptime":...}- Verify the server is running:
curl http://127.0.0.1:3000/health - Check that
SAFE_RM_APIin the client config matches the server'sBASE_URL - If behind nginx, ensure the proxy is forwarding correctly
- The HMAC secret does not match. Ensure
SAFE_RM_SECRETis identical on the server and client. - Check for trailing whitespace in the config file or
.env.
- Verify
which rmreturns/usr/local/bin/rm - Check that
/usr/local/binappears before/binin your$PATH - Verify the current session is guarded: set
SAFE_RM_ACTIVE=1to test, or check that your SSH source IP matchesALLOWED_SOURCE_IPS
- Check n8n execution logs for errors
- Verify the webhook URL in
.envmatches the URL shown in n8n - Test the n8n webhook manually:
curl -X POST https://your-n8n.example.com/webhook/delete-approval -H 'Content-Type: application/json' -d '{"request_id":"test","command":"rm -rf /test"}'
- The
BASE_URLin.envmust be publicly accessible from wherever you read your email - Check that the URL in the email matches the server's
BASE_URL - If running locally for development, use a tunnel (e.g., ngrok, cloudflared)
To remove safe-rm from a VPS:
sudo ./client/uninstall.shThis removes the binary, symlink, and profile script. You will be prompted about removing the config file. The system rm at /bin/rm is never modified and remains intact.