Most AI chat apps send every prompt to someone else's servers. I wanted the opposite: a chat app where the model runs on my hardware, works offline, and takes one command to start.
npx unmuzzled
That's Unmuzzled. It's open source (MIT), and this post covers how it works and the problems I didn't expect.
A real run on my laptop. The reasoning streams first, then the answer. Generation is sped up 8×; the real speed (1.7 tok/s) is shown at the end.
What it does
- Opens a chat UI in your browser. On first run, a setup screen shows the model, its size, your RAM and free disk, and downloads nothing until you click Download (with pause and resume).
- Runs the model locally through llama.cpp via node-llama-cpp, using Vulkan, CUDA or Metal when available.
- Streams answers with Markdown and syntax highlighting, and shows the model's reasoning live in a collapsible, timed panel.
- Supports editing, regenerating and searchable history. Chats are stored only in your browser.
- Works with any GGUF model:
npx unmuzzled --model ./your-model.gguf.
Why it's different
There are great local-AI tools already. Most of them assume you're comfortable with a terminal, model formats and config files. Unmuzzled is built for the person who just wants a private AI that works:
- Nothing to set up. No Python, Docker or config files. One command on desktop, one tap on Android. The app checks your RAM and disk and fetches the right file; you never need to know what "Q4_K_M GGUF" means.
- Private by design. The model runs on your device. After the one-time download it works in airplane mode, and there is no account, telemetry or server.
- No refusal filters. The default model has had its refusal training removed (details below), so it answers questions mainstream assistants decline. That's useful for research, security work and fiction, and it means the judgement is yours.
- Same experience everywhere. The desktop and Android apps share the same design, the same model and the same reasoning view.
The model
The default is a 9B model with an interesting family tree:
- Qwen3.5-9B, the base model
- Jackrong/DeepSeek-V4-Pro-Qwen3.5-9B, fine-tuned on ~250k reasoning samples generated by DeepSeek-V4-Pro
- xtools-at/…-UNCENSORED-heretic, with refusal behaviour removed using Heretic
- mradermacher/…-i1-GGUF, quantised to Q4_K_M (5.63 GB)
Every step is Apache-2.0. Despite the name, it is not DeepSeek's model; it's a small Qwen model trained to imitate DeepSeek's reasoning style. Unmuzzled doesn't redistribute the weights; they download straight from Hugging Face.
Because refusal training has been removed, it answers things other assistants decline, and like any LLM it can be confidently wrong. The README has a responsible-use section, and any model with safety training intact works too.
How it works
The whole thing is deliberately small: no framework and no build step on the desktop side.
bin/unmuzzled.js CLI: arguments, setup state, model download, startup
src/server.js HTTP server, static files, streaming chat API (SSE)
src/engine.js node-llama-cpp wrapper, reasoning/answer separation
src/models.js model catalog, download with pause/resume
public/ the interface: plain HTML, CSS and ES modules
Streaming. The browser POSTs the conversation to /api/chat, and the server streams Server-Sent Events: thought chunks, text chunks, and a final done event with token count and tokens/second. node-llama-cpp separates the model's reasoning into segments; for templates that don't, a small state machine splits <think>…</think> out of the raw stream, including tags that arrive split across chunks.
The UI. I wanted it to look like a carefully made desktop tool rather than a template: warm neutrals, one muted accent, IBM Plex Sans and Mono served locally (so it works offline), hand-drawn icons, and no gradients or glow. Model output is untrusted, so raw HTML is escaped and remote images are never fetched.
The part I didn't expect: localhost security
A local AI server feels private, but any website you visit can try to talk to 127.0.0.1. Two attacks matter:
-
CSRF-style requests. A page can fire a
text/plainPOST at your local server without a CORS preflight. Unmuzzled requiresapplication/json, which forces a preflight the server never approves. -
DNS rebinding. A malicious domain can resolve to
127.0.0.1after the page loads, making the browser treat your local server as same-origin. Unmuzzled rejects any request whoseHostheader isn't a local name.
The same guard protects the setup endpoints, so no website can start a 5 GB download on your machine.
Honest numbers
On my laptop (Intel i5-1135G7, Iris Xe integrated graphics via Vulkan, 16 GB RAM), the 9B model runs at about 2 tokens/second (1.7–2.7 across my runs) after a ~40–50 second load. "Offline" doesn't mean "fast"; hardware is the whole story. A discrete GPU or an Apple Silicon Mac is many times faster. Reasoning models also generate their thinking before the answer, which adds to wait time on slow hardware.
Shipping it on Android
The Android app (beta) runs the same uncensored model on the phone itself. It's not a thin client for a server or your laptop; once the model is downloaded, the phone needs no network at all.
What using it looks like:
- Open the app. It checks your phone's memory and free storage and tells you whether the 9B model will fit (it needs about 12 GB of RAM, which current flagships have).
- Tap Download. The 5.6 GB model downloads with a progress bar, speed and time left. You can pause it, and if you leave the app it saves its place and resumes when you come back. The screen stays on until it's done.
- Chat. Replies stream in with Markdown and a collapsible reasoning panel. A Think toggle in the composer switches between deep reasoning and instant answers.
Already have a GGUF file? "I already have a .gguf file" imports it from the phone's storage instead. Chats, settings and the model live only on the device, and there's no sign-in.
Under the hood it's React Native (Expo) with llama.rn. It ships llama.cpp builds tuned for modern ARM cores (dotprod/i8mm) and optional OpenCL offload on Adreno GPUs. Download the APK (beta). Open it on the phone and allow installs from that source when asked.
Building it on Windows was its own adventure:
- Gradle couldn't download anything until I forced Java onto IPv4. The network's IPv6 was broken, and Java tried it first.
-
A Markdown library imported Node's built-in
punycode, which doesn't exist in React Native, so I installed the standalone package. - Windows' 260-character path limit broke the native build: one library's generated C++ paths reached 297 characters, and Android's bundled Ninja 1.10 can't use long paths. I replaced the library with a 30-line Kotlin module that reads the system bar insets directly.
The app goes on GitHub Releases rather than Google Play. An app built around an uncensored model doesn't fit Play's AI-content policy, and side-loading an open-source APK is a well-trodden path.
Try it
npx unmuzzled # setup screen, then chat
npx unmuzzled --model ./model.gguf # bring your own model
If you run it, I'd love to know your tokens/second and hardware. Issues and PRs are welcome.
💬 Discussion on LinkedIn: join the conversation here

Top comments (0)