A dependency-free TypeScript daemon on your machine, driven over stdio by either a VS Code fork or a plain chat app. Per-agent model binding, five permission modes with three rails you cannot unlock, and a copy-on-write overlay so a diff exists before your files move.
Windows builds today · bring your own key or your existing CLI subscription
At a glance
.monoprise/ghost/, only
changed files stored. Diff, then apply, then a checkpoint you can
rewind to.
.monoprise/permissions.md, re-read on every call. An
unknown mode falls back to asking, never to allowing.
.monoprise/mcp.json, plus per-role
rule files and on-demand skill files under
~/.monoprise/.
Architecture
Everything that decides anything lives in the daemon. The hosts own their UI and their filesystem, and nothing else. The daemon cannot tell which of the two launched it — that was the design goal, and it is what keeps the two apps from drifting.
HOST · EDITOR
Monoprise IDE
A VS Code 1.127.0 fork. The team lives in a panel extension; the extension owns the webview, the workspace-trust gate, key storage and the daemon's lifetime.
HOST · CHAT
Monoprise Chat
Plain Node with zero dependencies — a local HTTP and SSE surface plus a browser UI. It honestly refuses the two editor-only tools rather than faking them.
SHARED DAEMON
TypeScript · zero dependencies · child process
Routing, orchestration, the permission gate, the overlay and
checkpoints, the vendor-CLI contract and the audit log. It holds
no keys and never touches your filesystem: it emits
execute_tool and waits for tool_result.
MCP is the single exception, because the daemon spawns those
servers itself.
Why there is nothing to install. The daemon is spawned on
process.execPath — the runtime the desktop app is
already running on. That one decision is the entire story of the
zero-prerequisite install. An earlier version of this daemon was
Python over a WebSocket port, and the very first outside tester got
stuck on installing Python 3.10.
Sessions are folder-scoped. An agent is pinned to the first folder it is handed, and changing folder restarts it. The chat app keeps per-folder transcripts keyed by a hash of the lowercased path, so reopening a folder reopens that folder's last conversation.
Connections
A subscription is bought by a person and follows that person. Bind it to a role instead and the moment two agents share a role, one of them is spending out of the other's wallet.
| Agent | Role in the run | Connection | Model |
|---|---|---|---|
| Rosa | Planner | Claude Code subscription | opus |
| Dane | Builder | Anthropic API key | sonnet |
| Iris | Builder — second bid | Codex subscription | gpt-5.6-terra |
| Otto | Reviewer | Groq API key | your pick |
Dane and Iris hold the same role and bid against each other on the same task — on two different accounts, because the binding is per agent. Three slots sit outside this map on purpose: Judge, Chat and direct run, since none of them is a pipeline stage and an agent-name map would collide the day someone names an agent “Judge”.
| Vendor | CLI subscription | API key | Notes |
|---|---|---|---|
| Claude Code | ✓ | ✓ | The only one of the four that accepts a spend cap on a turn. |
| OpenAI Codex | ✓ | ✓ | Detection stops at “installed” — its CLI exposes no login probe. |
| Google Antigravity | ✓ | ✕ | Refuses key mode outright. The binary would ignore the key and bill a different account, and silently billing the wrong account is worse than refusing. |
| Gemini | ✕ | ✓ | Key only, permanently. The interactive Google login in the old Gemini CLI was retired in June 2026, and reviving it from a third-party app gets accounts banned. |
| Anthropic · OpenAI · Groq | — | ✓ | Direct API keys, billed by the provider to you. |
| Custom endpoint | — | ✓ | Any OpenAI-shaped endpoint: set a base URL and it goes there instead. Presence of a base URL is what makes a key “custom”, checked before every other rule. |
HOW A PASTED KEY IS ROUTED
Detection, connection and the actual gate are three different
things.
Detection is a harmless --version probe, so it only
knows a CLI is installed — never whether you are logged in. The
“connected” list that populates the dropdowns is client-side state,
an affordance and not a wall: if you connected a subscription and it
is not in the list, run the connection test once and it appears.
The wall is in the daemon. Before every run it takes a
no-charge probe of the backend an agent is bound to and
refuses the turn if that backend is not runnable — memoised for 45
seconds so it costs you nothing. Login state is answered as
true, false or null, and
null means nothing answered the question. It does not
guess.
The paid half of the connection test — one real round trip that
answers with OK — never runs unless you explicitly
consent, and when it does it runs read-only. A button that verifies
a connection must not be able to change a file.
Permissions
The modes decide how much you are asked. They never decide what is checked. When you say “unattended” you are saying don't ask me — you are not saying don't check.
| Mode | Ordinary work | Risky operations | On the CLI path |
|---|---|---|---|
manual |
Asks about nearly everything | Asks | Demoted to read-only |
ask |
Reads pass; anything that writes or runs asks | Asks | Edit |
auto
· default
|
Passes | Asks | Edit |
fullauto |
Passes | Destructive ones are refused, not asked | Edit |
unattended |
Passes | Passes every soft gate — the three rails still hold | Edit |
Two details worth knowing. An unrecognised mode string resolves to
ask, never to allow. And our
unattended is deliberately not wired to a vendor
CLI's full bypass flag — “don't ask me” is not “unlock the vendor's
own locks”.
All three are evaluated before the mode gate is even consulted. There is no mode, rule or flag that turns them off.
OUTSIDE THE WORKSPACE
Paths are canonicalised to a single spelling first — NTFS
alternate data streams, drive-relative C:..\, trailing
dots, \\?\, junctions — and then anything landing
outside the open folder is denied.
ITS OWN RULE FILES
It may read .monoprise/ but never write or delete
inside it. A gate that can edit its own rule file is a lock that
has handed over its key. Reading stays allowed so it can explain a
refusal instead of retrying it.
YOUR DENY RULES
A matched deny returns before the mode is read. In
unattended, where there is no “ask” to fall back to,
a command naming a denied path is refused outright. Deny wins even
in unattended.
.monoprise/permissions.md — RE-READ ON EVERY CALL
Two files layer: ~/.monoprise/permissions.md globally and
<workspace>/.monoprise/permissions.md per project.
Both are re-read on every call, so an edit takes effect on the next
action with no restart. A path deny blocks reads as well
as writes — a rule that stops a write but lets the file be read to a
model has blocked the cheap half and allowed the expensive one.
Three things are classified risky, and in auto they stop
and ask you:
No exceptions and no size threshold.
We have no idea what a third-party server does, so all of them
are risky by default — including in fullauto, where
they are asked rather than refused so a working server isn't
bricked.
Recursive removes on every shell dialect, disk-level tools,
dd, mkfs, sudo,
chmod 777, git push --force, fork
bombs, and anything piping a download into an interpreter. Shell
metacharacters count too, and so does any command that names a
path you denied.
This list leaks, and we say so. A blocklist of dangerous
command shapes is a memory of past accidents, not a boundary. It is
deliberately narrow — rm -f one-file.txt is not treated
as risky — and it never substitutes for the three rails above.
Anything that depends on this list catching everything is depending
on the wrong thing.
Change flow
This is the part that makes leaving it running defensible. Writes land in an overlay, you get a real diff against your working tree, and only an explicit apply moves anything — with a checkpoint recorded first.
Every write from a risky tool is redirected into
.monoprise/ghost/<agent>/. Only changed files
are stored — there is no copy of your repository, and read
paths fall through to the real file so the agent sees a coherent
tree.
Per file: status, additions, deletions, binary flag and the patch itself. Long diffs are truncated for display, but the counts are always real — only the text is cut. The preview is exempt from the execution lock on purpose, because it is read-only and must answer while other work is still running.
Discard throws the overlay away and your tree was never touched. Apply writes through — and unlike the preview, both apply and rewind take the execution lock.
A checkpoint's manifest is written before the first overwrite, and it records created files as well as modified ones — because restoring a backup cannot express “this file did not exist before”. Rewinding puts back what changed and removes what appeared.
If a file changed between you reading the diff and pressing the button, that file is refused by name rather than overwritten. Restores report what they refused, not just a count. A silent overwrite here is data loss wearing the mask of a successful restore.
The overlay does not exist on the subscription path — and the app says so before every turn. When a vendor CLI runs your turn, it edits real files with its own tools in its own process. There is no per-tool permission decision, no confirmation card and no overlay on that route. What remains is rollback after the fact, from a byte-level snapshot taken before the turn. If that snapshot cannot be taken, the turn is refused rather than run unprotected.
The snapshot uses a temporary git index, so it touches neither your
real index, nor HEAD, nor your worktree, and it captures untracked
files — which is exactly why git stash create was
rejected for the job. Byte-identical restore needs git 2.40 or newer
when a .gitattributes is present; on older git it
returns a warning instead of claiming an identity it cannot
guarantee.
| Stage | Grade | Why |
|---|---|---|
| Builder | edit |
The only stage that needs to write. |
| Planner · Reviewer · Judge | readOnly |
A reviewer that can edit is not a reviewer. On the strictest vendor this comes up triple-locked. |
| Direct run | edit |
You addressed a CLI yourself, so it keeps its full toolset — which is why it is a separate slot rather than reusing the pipeline's. |
Where a vendor's read-only mode rests on a single lock rather than several, the app announces that before it spawns the process. You are told how thin the guarantee is, in the moment it matters.
Local and secrets
There is nothing to opt out of, because there is nothing to opt into. Your code is read and written locally, and requests go from your machine straight to the provider you configured.
| Thing | Chat app | IDE |
|---|---|---|
| API keys | Process memory only. Gone when the app closes. | VS Code SecretStorage only. Never in settings state. |
| Saved on disk | Which backend you chose — never the key itself. | Same: the record holds vendor, mode and model, and no credential. |
| Returned to the UI | The shape, never the secret. | Usage figures and labels only. |
| Subscription credentials | Never touched. In subscription mode nothing is passed and the vendor CLI finds its own login — and billing-related environment variables are stripped from the child process so an ambient one cannot redirect the charge. | |
| Audit log | Written to be shown to someone else: secrets are scrubbed from it, and the scrubber is tested against a corpus of fake credentials — including over-redaction, so it doesn't destroy the record it exists to keep. | |
~/.monoprise/ — ALL PLAIN TEXT, ALL RE-READ EVERY RUN
Skills are why this does not turn into prompt bloat: only a one-line
description of each stays resident, and the body is fetched when the
model asks for it — so adding more skills does not cost more tokens
per turn. Trusted commands name a program and the mode it may run
in, because node --check -r ./payload.js app.js ran
arbitrary code once, and once was enough.
What we will not claim. Two masking gaps in the audit scrubber are open and tracked: passwords embedded in URL userinfo that are short and unremarkable, and unnamed AWS secret access key values. Webhook forwarding uses the same scrubber, so it leaks the same way. We count this as “this much is open”, not as “closed”.
Model choice and extension
PER-ROLE DEFAULTS
Each role gets a recommended model from a tier table, and you can change it. Quality roles have their own defaults, because a lightweight chat default leaking into planning and review is a silent downgrade nobody notices — the UI warns you if you assign a lite or mini family model to anything but chat.
WHEN A MODEL FAILS
Models that already died this session are skipped before they are tried again, and a 404 or an exhausted quota gets exactly one automatic fallback. Rate limits are read rather than blanket retried: a short retry-after means wait, a long one means switch.
SUBSCRIPTION MODELS
Model names on a subscription come from a fixed dropdown, and an empty value means the account's own default. A name starting with a dash is rejected outright, so a client-supplied string can never become a command-line flag.
SPEND VISIBILITY
Usage is taken from what each call actually reports, accumulated per key, and a key over its limit is dropped from requests rather than left to fail. Providers expose no account-balance API, so local accounting is the only honest option.
Declare servers in <workspace>/.monoprise/mcp.json and
the daemon spawns them itself — the one place it does anything without
going through the host. Their tools appear alongside the built-in
ones, they can be denied by name with the same
$ prefix as a shell rule, and they are treated as risky
in every mode. Note one current limit before you wire up a chatty
server: MCP results are cut at 40,000 characters from the front, so a
longer result arrives with its tail already gone.
How it's kept honest
Two apps implementing the same logic twice, with no shared code, drift on every pass. So the drift was made loud: shared tables are named, and a suite fails by table name when the two sides disagree.
PARITY
29 shared tables are marked with a named anchor — key-shape detection, permission modes, secret patterns, model tiers, the connection gate and the rest. The parity suite does not compare strings: it extracts the declarations from both apps, compiles them, and runs them against a common corpus, so it compares behaviour. It carries 353 assertions.
Honest footnote: 22 of the 29 anchors appear by name in that suite. Four have no section at all. An anchor marks that a copy exists — it is not itself a lock.
VENDOR CLIS
Calling a real vendor CLI in a test burns the developer's own subscription, so the suite never does. Four fake vendor executables — shell and .cmd pairs — are put on PATH, and the product picks them up by name with no knowledge that anything is staged.
Their behaviour is switched by environment variable only, because the argument vector is the thing under test. And they never report their environment back, so a developer's real key cannot land in a temp file.
ESCAPES
The sandbox suite tries the escape for real and then checks the filesystem, rather than asserting on an error message — because a refusal string can be returned by a call that also succeeded in writing.
PACKAGING
The packaging test starts the packaged artifact and waits for a handshake, so “it builds” means the thing that ships actually speaks. It also asserts the process spawned is the application itself, not some interpreter found on the machine.
Vendor CLI versions the integration was measured against:
Claude Code 2.1.143, Gemini 0.51.0, Codex
0.147.0, Antigravity 1.1.12. Prompts are
always handed to a vendor CLI on stdin, never on the command
line — Windows caps a command line at 32,767 characters, and going
through the shell hands your prompt to the shell's own parser.
Where we are
Written the way we'd want to read it if we were evaluating somebody else's tool.
And the thing that will actually bite you first: editing the source does not change a portable build. The daemon only loads new code when the app restarts, and a freshness check refuses to assemble a build whose sources are newer than its compiled output — except on one installer path, which is a known gap we are closing.
Leave your email and we'll send you the build within a day. Tell us what you'd point it at — the sharper the setup, the more useful your report back.
Beta · free · no card
Prefer to just email us? monoprise@monoprise.dev
Used only to send you the beta build. No newsletter, nobody else sees it.