1 MCP Policy Guide
megaproxy edited this page 2026-05-26 18:49:02 +01:00

MCP Policy Guide

How tiletopia's MCP permission model works, and how to tune it.

Three-tier model: allow / ask / deny

Every MCP write call (anything that mutates state) gets matched against three rule buckets in this order. Deny-first precedence: a matching deny rule wins over any allow.

Bucket What happens on match
deny Call is refused server-side with denied: <reason>. Claude sees the error and can adapt or apologize.
ask Confirm modal pops in tiletopia. You Approve / Reject / Always-Allow per call.
allow Call runs silently. No modal. Audit log still records it.

Default policy is empty. No allow / ask / deny rules out of the box. Every non-hard-denied call falls through to "no matching rule → ask" — the most restrictive default.

Configured in the panel's Policy tab; persisted to %APPDATA%\com.megaproxy.tiletopia\mcp-policy.json.

Rule syntax

A rule is either a bare tool name or tool_name(glob):

Example rule What it matches
set_label Any set_label call regardless of args
write_pane Any write_pane call regardless of text
write_pane(git push *) Only write_pane calls whose text starts with git push
write_pane(rm -rf /home/me/scratch/*) The exact path prefix
apply_preset(two_columns) Only apply_preset calls naming the two_columns preset
write_pane(* main) Anything ending in main (useful for blocking git push origin main)

Globs use * as wildcard only — no regex, no character classes. The glob matcher is shell-operator-aware: a rule fires if any subcommand of the input matches it. So write_pane(rm *) matches echo hi && rm tmp.txt. This is intentional so a single rule guards the obvious destructive pattern without you listing every shell-operator variant.

SSH safeguards (three switches in the Policy tab)

SSH gets extra gates above the regular policy because the bytes leaving tiletopia get interpreted by a remote shell — alias expansion, subshells, and ~ expansion all happen on the other side, where local hard-deny patterns can't see.

Switch Default What it gates
allow_open_ssh off connect_host and spawn_pane(kind=ssh). When off, Claude can't open SSH sessions at all — you do it manually via the titlebar 🔑 picker.
auto_allow_spawned_ssh off Whether SSH panes Claude spawns start with the 🤖 chip on (visible to MCP). When off, you must toggle each one explicitly.
allow_add_host off add_host and delete_host — Claude editing the saved-hosts list.

Stacked off, Claude can do nothing SSH-related on its own. The user manages SSH through the UI; Claude only sees panes you've already opened AND chosen to allow.

add_host's extraArgs are sanitised even when allow_add_host is on — Claude can't sneak in -o ProxyCommand=..., -o LocalCommand=..., -o KnownHostsCommand=..., or -o PermitLocalCommand=yes. Those are all CVE-2023-51385-class local-RCE primitives that run on connect.

Hard-deny: the patterns you can't disable

A compiled-in list of 14 catastrophic patterns is checked against every write_pane text before any user policy. The user can NOT disable these. They're visible (greyed out) in the Always blocked section at the bottom of the Policy tab.

POSIX shell (10)

Label Catches
rm -rf / rm -rf / and case variants (-Rf, -Rrf), with trailing shell operators or # comments.
rm -rf ~ rm -rf ~, rm -rf $HOME.
rm -rf /* The glob form.
fork bomb :() { :|:& }; : and whitespace variants.
mkfs on device mkfs.<fs> /dev/....
dd to raw disk dd of=/dev/{sd,nvme,hd,disk}....
overwrite system auth file ... > /etc/{passwd,shadow,sudoers}.
pipe to shell from network curl ... | bash, wget ... | sudo sh, curl ... | zsh, etc.
chmod -R 777 / Anchored to the literal / root (no false-positive on /tmp).
find / -delete find / ... -delete.

PowerShell / cmd (4)

Label Catches
Remove-Item C:\ / ~ Remove-Item -Recurse -Force C:\, del C:\, rd /S /Q ~, etc. Also matches $env:USERPROFILE / $env:APPDATA targets.
Format-Volume / Clear-Disk Bare cmdlet mentions are fine (e.g. Get-Help Format-Volume); presence of a flag triggers.
iwr | iex (PowerShell) Invoke-WebRequest | Invoke-Expression pipe form.
iex (irm ...) (PowerShell) iex (irm ...) parenthesised form.

Important caveats

Hard-deny is best-effort accident prevention, not a sandbox. Bypasses exist:

  • \rm, aliases (alias rm=...), variable indirection ($SHELL -c 'rm -rf /').
  • Anything that happens after the bytes reach the shell: ~ expansion, command substitution, eval, sourcing files.
  • SSH targets — the remote shell sees only the bytes we send; nothing local can introspect what they'll become.

The list catches honest mistakes and obvious copy-paste attacks. A determined attacker who already has write access to the shell can defeat it.

Worked examples

"Let Claude rename and reshape, ask before anything else"

{
  "allow": ["set_label", "promote_pane", "swap_panes", "apply_preset"],
  "ask": [],
  "deny": []
}

Everything else falls through to the default ask.

"Allow Claude to send any non-destructive bash, ask before destructive"

{
  "allow": ["write_pane(*)"],
  "ask": [
    "write_pane(rm *)",
    "write_pane(git push *)",
    "write_pane(sudo *)",
    "write_pane(* --force*)"
  ],
  "deny": []
}

The ask rules win over the broad allow because of deny-first → ask-second precedence. (Allow being a * glob doesn't override more specific asks.)

"Block all write_pane on production hosts"

You can't directly key rules on shell-kind today (it's a known follow-up). Workaround: target by the literal hostname in the text:

{
  "deny": [
    "write_pane(* prod-*)",
    "write_pane(* deploy *)"
  ]
}

"Trust Claude with write_pane on local panes, ask on SSH"

Same gap. Workaround today is the broader auto_allow_spawned_ssh safeguard set to off, which keeps every Claude-spawned SSH pane out of MCP visibility unless you flip its chip. So Claude can't write_pane to SSH without you opting in per-pane.

Audit log

Every tool call — denied, asked, or allowed — appears in the panel's Audit tab.

  • Last 200 entries, ephemeral (cleared on app restart).
  • Each row: timestamp, tool name, truncated args summary (80 chars), result (ok / denied / failed), duration.
  • write_pane args are truncated and control-chars escaped so pasted tokens don't leak into the UI verbatim.

If Claude does something surprising, the audit log is the first place to look.

Files on disk

  • %APPDATA%\com.megaproxy.tiletopia\mcp-policy.json — your rules + safeguards. Atomic tmp+rename writes; safe to back up or sync.
  • %APPDATA%\com.megaproxy.tiletopia\mcp.json — port + bearer token. Don't share.
  • %APPDATA%\com.megaproxy.tiletopia\hosts.json — saved SSH hosts (no passwords; those are in Windows Credential Manager).

See also