Table of Contents
- MCP Policy Guide
- Three-tier model: allow / ask / deny
- Rule syntax
- SSH safeguards (three switches in the Policy tab)
- Hard-deny: the patterns you can't disable
- Worked examples
- "Let Claude rename and reshape, ask before anything else"
- "Allow Claude to send any non-destructive bash, ask before destructive"
- "Block all write_pane on production hosts"
- "Trust Claude with write_pane on local panes, ask on SSH"
- Audit log
- Files on disk
- See also
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_paneargs 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
- MCP Setup for Claude Desktop — getting the
.mcpbbundle installed - Troubleshooting — connectivity issues if Claude can't reach the server