Everything a local LLM app has to refuse
Binding to localhost is where it starts. Once models can fetch pages, write files and run commands, most of the design is deciding what not to do.
Switchbox is a local web app for putting the same question to several models at once. At its core it is four chat panes and a composer. But once models can use tools, fetching a page, writing a file, running a command, most of the design is deciding what the app has to refuse, and where.
Local is not the same as private
The server binds to 127.0.0.1, so nothing on the network can reach it. That is necessary and not nearly enough, because the browser on the same machine can reach it, and so can every web page open in that browser.
A page on some other site can make the browser send a request to localhost:8787. It can also point a hostname it controls at 127.0.0.1 so that its requests look same-origin, which is DNS rebinding. So the API refuses any request whose Host is not localhost or 127.0.0.1, refuses requests sent from other websites, and only accepts application/json bodies. That last rule sounds pedantic until you remember that a plain HTML form can POST across origins without any preflight, and it cannot send JSON.
A fetch tool is a request-forgery tool
web_fetch lets a model read a page by URL. Give that to a model that is reading untrusted text and you have handed the internet a way to make requests from inside your network. So the tool refuses localhost and private-network addresses, and the check follows redirects and DNS resolution. A public URL that redirects to 192.168.1.1, or a hostname that resolves to it, is refused just like the address itself.
What comes back from the web, or from an MCP server, is wrapped in <tool_output> tags, and the system prompt tells the model to treat it as information, not instructions. That does not make prompt injection impossible. It makes the boundary explicit to the model, which is the part the app controls.
Paths that stay inside
A chat can have a workspace, a folder that models can list, read, write and edit. Every path is checked to stay inside it, and that means refusing more than .. and absolute paths. On Windows it also means device names like CON and NUL, alternate data streams such as notes.txt:hidden, and symbolic links or junctions that lead out of the folder.
Serving those files back to the browser has a trap of its own. A model can write an HTML or SVG file, and if the app serves it as HTML, it runs with the app's origin. So workspace files are served only as images or plain text, sandboxed, with content sniffing turned off.
Commands, honestly
The Run commands tool starts a real shell in the workspace folder, and it is not sandboxed. The README says so plainly: an approved command can read or change anything your user account can. Pretending otherwise would be worse than the risk.
What the app can do is narrow it. run_command asks before every run by default, and the approval card shows the command line itself, not a summary. API keys and the rest of .env are stripped from the command's environment, and only what shells and compilers need, such as PATH and HOME, is passed on. stdin is closed, so a program waiting for input cannot hang forever. A 60-second timeout kills the process and everything it started, and stopping the reply does the same. Output is capped, keeping the beginning and the end. For real isolation the advice is to run all of Switchbox inside a container or a VM.
The one sandbox that is real
run_js, the tool models use for arithmetic, dates and data wrangling, is a sandbox. It runs in a worker thread with a memory cap and a five-second limit, in a V8 context with no Node globals: no require, no process, no network, no files. Compiling code from strings is disabled, so eval and new Function cannot rebuild what was taken away.
Auto, Ask, Off
Every tool has one of three policies. Auto runs it. Ask pauses the reply and shows the call's arguments until you allow or decline it. Off leaves it out of the model's tool list altogether. Most built-in tools default to Auto; MCP tools and run_command default to Ask. A declined call is reported back to the model as declined, so it can carry on instead of retrying blindly.
Importing an export never changes the Run commands policy, and MCP servers that arrive in an import start switched off. Settings that decide what runs on your machine should not arrive in a zip file.
Skriven av Martin Dahl. Om du vill diskutera något av det här, lämna ett meddelande.