Skip to content

Hardening

The security model covers what netclaw enforces by default: default-deny, audience scoping, a four-layer invocation stack. This page covers what you should do on top of that, especially if you’re running it for a team, exposing it to the internet, or leaving it unattended.

Sections are independent. Work through the ones that apply to your deployment.

All configuration lives in ~/.netclaw/config/netclaw.json unless noted otherwise. Values merge with built-in defaults, so you only need to specify what you’re changing.

Netclaw stores credentials and webhook secrets in plaintext JSON files. Lock them down:

Terminal window
# Encrypted secrets — only the daemon user should read this
chmod 600 ~/.netclaw/config/secrets.json
# Webhook route files contain HMAC secrets
chmod 700 ~/.netclaw/config/webhooks/
# Encryption keys for secrets at rest
chmod 700 ~/.netclaw/keys/

netclaw doctor checks secrets.json permissions and flags unencrypted values. Run it after any manual config edits.

Shell access is the biggest risk surface you can control. Three modes in ~/.netclaw/config/netclaw.json:

ModeBehavior
OffShell completely disabled. No shell_execute tool available.
SandboxOnlyShell runs in a sandboxed environment.
HostAllowedShell runs directly on the host. Approval gates are your only guardrail.

Set the mode explicitly:

{
"Security": {
"ShellExecutionMode": "Off"
}
}

For Team and Public postures, shell is off by default. Only Personal posture enables it, and only with approval gates on shell_execute. If you’re running Personal posture but don’t need shell, turn it off.

The built-in hard-deny list blocks sudo, rm -rf ~/, kill, fork bombs, and commands that would stop the daemon. Add your own patterns for anything dangerous in your environment:

{
"Tools": {
"HardDenyPatterns": [
"docker rm",
"kubectl delete namespace",
"terraform destroy"
]
}
}

Custom patterns augment the defaults, they don’t replace them. Netclaw tokenizes compound commands (&&, ||, ;, |) and checks each segment independently, so echo hello && docker rm foo still triggers the deny.

New MCP servers start with zero tool grants for all audiences, safe by default. The risk comes from granting too much.

Audit your grants per audience:

Terminal window
netclaw mcp permissions

MCP tool grants for Personal audience showing all tools enabled

Personal audience with all tools granted. Compare against the locked-down Team and Public defaults:

Team audience with server disabled

Team audience: server disabled, no tools granted.

Grant the minimum tools each audience actually needs. Don’t enable everything for Team because it’s faster to configure. For destructive MCP tools (delete, drop, write), set approval mode to Approval or Deny. Per-tool overrides let you keep the server default on Auto while gating the dangerous ones:

{
"Tools": {
"AudienceProfiles": {
"Personal": {
"ApprovalPolicy": {
"DefaultMode": "Auto",
"ToolOverrides": {
"shell_execute": "Approval",
"notion/notion-delete-page": "Approval"
}
}
}
}
}
}

Tool override keys use the format {serverName}/{toolName}.

Review ~/.netclaw/config/tool-approvals.json periodically and prune stale “approve always” decisions.

See netclaw mcp for full details on the permissions TUI and CLI.

The daemon binds to 127.0.0.1:5199 by default. Keep it that way unless you have a reason not to.

ModeScopeRisk
localLoopback onlyMinimal — only local processes can connect
tailscale-serveYour tailnetLow — Tailscale identity gates access
tailscale-funnelPublic internetHigh — anyone on the internet can reach it via Tailscale Funnel
cloudflare-tunnelPublic internetHigh — requires a Cloudflare Access policy

Exposure mode selection during netclaw init

The exposure selection during netclaw init. Internet-facing modes force explicit confirmation.

If you need remote access, prefer tailscale-serve. It limits access to your tailnet. Funnel exposes you to the public internet — a completely different threat model.

For Docker deployments, bind to loopback explicitly:

Terminal window
docker run -p 127.0.0.1:5199:5199 ...

Omitting 127.0.0.1 binds to all interfaces. Any machine on your network can reach the daemon.

Changing exposure mode requires a daemon restart. It’s excluded from hot-reload on purpose.

Terminal window
netclaw daemon stop && netclaw daemon start

Out of the box, Slack uses MentionOnly: true (netclaw only responds when @mentioned) and AllowDirectMessages: false. Lock down channel and user access on top of that.

With no channel allowlist and no default channel set, netclaw denies all channel traffic. Explicitly list the channels it should respond in:

{
"Slack": {
"AllowedChannelIds": ["C0123ABCDEF", "C0456GHIJKL"],
"MentionOnly": true
}
}

To find a Slack channel ID: right-click the channel name in Slack, select “View channel details,” and look at the bottom of the panel. Or see Slack’s guide to finding IDs.

Restrict which users can invoke netclaw:

{
"Slack": {
"AllowedUserIds": ["U0123ABCDEF", "U0456GHIJKL"]
}
}

User IDs follow the same pattern — click a user’s profile in Slack and find the ID under “More.”

Keep DMs off unless you need them. If you enable DMs, restrict which users can DM:

{
"Slack": {
"AllowDirectMessages": true,
"AllowedUserIds": ["U0123ABCDEF"]
}
}

You can also override the audience per channel. Useful if you want a specific Slack channel to get Personal-level tool access while everything else stays at Team:

{
"Slack": {
"ChannelAudiences": {
"C0123ABCDEF": "personal",
"dm": "team"
}
}
}

The "dm" key is reserved. It maps all direct messages to the specified audience.

Invalid audience values in ChannelAudiences result in a deny (fail-closed).

netclaw doctor warns if Slack is enabled with no channel allowlist and no default channel configured, or if DMs are enabled without an AllowedUserIds list.

The last layer before a tool actually runs. Configure per audience:

{
"Tools": {
"AudienceProfiles": {
"Personal": {
"ApprovalPolicy": {
"DefaultMode": "Auto",
"ToolOverrides": {
"shell_execute": "Approval"
}
}
},
"Team": {
"ApprovalPolicy": {
"DefaultMode": "Approval"
}
},
"Public": {
"ApprovalPolicy": {
"DefaultMode": "Deny"
}
}
}
}
}
  • Headless sessions (reminders, webhooks, netclaw chat -p "prompt") auto-deny all gated tools. There’s no human to ask.
  • No response within 5 minutes means deny.
  • “Approve always” persists to ~/.netclaw/config/tool-approvals.json. Revoke by editing the file directly.

If nobody is watching the approval prompts in production, set DefaultMode: "Deny" for Team and Public. Auto-deny is safer than a 5-minute timeout nobody sees.

Each webhook route has its own HMAC secret, audience, body size limit, and rate limit:

{
"Verification": {
"Kind": "Hmac",
"HmacAlgorithm": "Sha256",
"Secret": "whsec_...",
"SignatureHeaderName": "X-Hub-Signature-256",
"SignaturePrefix": "sha256="
},
"Audience": "Public",
"MaxBodyBytes": 1048576,
"RateLimitPerMinute": 10
}

Set the audience to the minimum the webhook actually needs. Most should run as Public (fewest tools, session-scoped filesystem, wiped on end). Only use Team or Personal if the webhook prompt genuinely requires those tools.

RateLimitPerMinute applies per webhook route, not globally across all routes. Lower it from the default 30 if the source won’t fire that often — GitHub sends roughly one webhook per event, so 10/min is plenty. Keep MaxBodyBytes at 1 MB or lower unless you know the payloads are larger.

Route files contain secrets in plaintext. Keep ~/.netclaw/config/webhooks/ at mode 700 (see File Permissions).

See netclaw webhooks for route setup and HMAC verification details.

After making changes, validate everything:

Terminal window
netclaw doctor

netclaw doctor running 16 diagnostic checks

Security-relevant checks include:

CheckWhat it catches
Security PolicyMissing DeploymentPosture
Secrets JSONWrong file permissions, unencrypted values
Slack ACLNo channel allowlist, DMs enabled without user allowlist
Tool Audience ProfilesOverly permissive tool grants
exposure-modeInternet-reachable exposure without valid auth policy
Inbound Webhook RoutesSchema errors in route files

Wire it into your deployment pipeline:

Terminal window
netclaw doctor --format json | jq -e '.exitCode == 0'
  • Prompt injection detection is regex-based. It catches known patterns (role resets, exfiltration attempts, invisible Unicode) but novel phrasings, Base64 encoding, synonym substitution, and non-English attacks can evade it. Treat it as a tripwire, not a firewall.
  • Tool grants are per-audience, not per-channel. ChannelAudiences overrides which audience a channel maps to, but you can’t give one Slack channel different tools than another channel with the same audience.
  • Secret redaction catches sk-*, Slack tokens (xox[baprs]-*), ghp_*, AWS access keys (AKIA*), JWTs, PEM blocks, and common JSON key names. Custom secret formats won’t be redacted. Use hard-deny path rules to block file access instead.
  • Approval gates only work on interactive channels. Headless mode, reminders, and webhooks auto-deny all gated tools. Design automation workflows with that in mind.