OpenOv AI

Permissions and safety

What the instance allows a session simply does; what it denies it never gets to try. Every seat starts in Claude Code's auto mode, as the CLI does: its classifier settles a call no rule settles, and most such calls run with no stop. A call a rule says to ask about, and in Manual every call no rule settles, stops the run and appears on that session's panel as a permission popup, in the session's own words. A permission popup nobody answers is a Deny after ten minutes, or two for a subagent's call, and the session is told that nobody answered: it goes on with what it can do without the call and asks for it again when you are back.

What a session may do from birth

A fresh instance seeds two settings files once and never touches them again, but for two things: an update wires the subagent hook, below, into a settings file that lacks it, taking out the shell hook versions before 0.26.0 ran, and turns the harness's commit attribution off in one that says nothing about it. It adds no rule: a rule a fresh instance of a newer version is born with and yours lacks is asked for on the Leader's panel instead, below. What a fresh instance is born allowing:

No interpreter is allowed by rule: a rule for node, bash or sh holds whatever program it is handed, so a script is the classifier's to judge in auto mode, or yours on a popup in Manual. An instance made before 0.26.0 keeps the rules it holds.

And what it is born denying:

And the two it is born asking for, both of them files that reach every session:

Every path rule starts with /, which anchors it at the instance directory, so a session that has stepped into a repository with cd is still allowed the same places. The attribution Claude Code would add to every commit and pull request is off: a commit is the seat's, made as you asked, and says what its message says. A settings file that says something else about attribution is yours, and an update leaves it.

The permission popup

Every seat starts in auto mode, as the Claude Code CLI does: the two bolt switches on the Leader's head, one for the Leader and one for the Workers, are on whenever the server starts. In auto, Claude Code's classifier settles a call no rule settles; a call it blocks is a row on the session's panel, and the session is told that a block is an answer, never something to spell another way. A switch turned off puts that role in Manual, where every such call asks on a popup, until the server stops; nothing about the switches is saved. Deny rules refuse what they name in either mode, and ask rules and Claude Code's protected paths raise a popup in either mode. The classifier's calls count toward usage the way they do in the CLI.

A call that asks stops the run and appears on that session's panel: Paul wants to use Bash, the command and the reason the session gave for it, or the path it was going to write. A call one of the session's subagents made says so under the heading, in the words the session started the subagent with — from its subagent Explore — “find the card CSS” — or from a subagent where the server did not see that subagent start. Three answers, always in this order:

Allow
lets that call through, once.
Always allow
where the request says plainly what the whole class of calls is, the permission popup lists the rules it saves under Always allow saves:, one to a line — Bash(make:*), a command by its first word (git by its subcommand: Bash(git push:*)), or Edit(/projects/app/**), a write by the directory it was in — and pressing it lets the call through and leaves the instance allowing that shape.
Deny
takes the sentence you type beside it. The call's error reads Ana denied this call on the panel: "…", under your name, and every session is told that a Deny comes back that way, is your answer rather than something the call's output made up, and is to be taken as an instruction rather than a reason to look for another way round.
the permission popup: Paul wants to use Bash, the command, the reason, and the rules Always allow saves

A command whose first word is a path or a variable gets no button, and neither does a file tool's write outside the instance — /tmp included, which is why a Worker keeps its files inside the instance — nor a tool the server serves, which is granted already. A shell rule matches the start of a command, not the paths it names, so an allowed cp or find runs in /tmp with no popup. A run that ends before it was answered takes its question down with it. While the page is not visible, the browser can notify you that a permission popup appeared, if you turned that on.

A popup that holds a session's call, a permission popup or a question, is a Deny after ten minutes nobody answers it, or after two when one of the session's subagents made the call: a subagent is not built to be stopped, and the session's prompt cache lives five minutes. It leaves the panel as an answered popup does, the panel says Waited 634 seconds for permission, timed out: …, and the session gets, as the call's error, that you did not answer the popup within that time. That Deny is nobody's answer: the session goes on with what it can do without the call, says what waits on it, and asks for it again once you are back, never looking for a way round it. The clock is read once a minute, so a popup goes within a minute after its time. When it was a Worker's popup, the Leader is told it timed out. The Leader and the Workers are held to it alike.

Where no rule is composed for a command, the button would carry the one Claude Code itself would save: the exact command, written out whole. That rule is not offered as Always when it is longer than 80 characters or holds | & ; < > $, a backtick or a newline — a one-off nobody would keep as a rule — and Allow and Deny answer the call. The rules composed for the sides of a compound, below, are offered whatever the command's length, and none is drawn from inside a backtick or $( span. A rule naming one file, Edit(/knowledge/common.md), is never offered as Always: it is the Leader's to settle, in words. Once a popup is answered after a wait, the panel row that says how long it waited — Waited 233 seconds for permission: … — shows the call's first line; double-click it for the whole call.

A question a session asks you, Claude Code's AskUserQuestion, is a popup of its own, answered on the session's panel as a form: a rule between questions, the options in a column, Other as the last option with a line of its own to type in, and Send and Cancel. It is allowed from birth, since the question is already the thing you are asked.

Compound commands

Claude Code matches a Bash rule against a command from its first character, so an instance that allows git, cd and npm would still be stopped on cd app && npm run lint && make dist: no one rule holds the whole. In auto mode the classifier settles such a line; in Manual it stops on a popup.

The permission popup offers one rule per side the instance does not hold yet: cd app && npm run lint && make dist offers Always allow Bash(make:*), cd and npm being held from birth — and never a rule for the words Claude Code runs without asking anyway (ls, cat, grep, cd and the rest of its built-in read-only set). A side whose first word is a path, a variable or a word of the shell's own, such as for, makes the popup offer nothing for the whole line.

No hook runs before a shell command. The one that let a chain of allowed commands through, and refused other chains toward a script, ran until 0.26.0; an update takes it out of .claude/settings.json.

A subagent a session starts with the Agent tool is given none of the instance's rules by Claude Code. A hook, lib/hooks/subagent.mjs, named in the settings as sh "${CLAUDE_PROJECT_DIR}/lib/hooks/run.sh" subagent so it runs on the Node.js the instance's version pins, runs when one starts and hands it customization/common.md, word for word, in the frame a seat reads it in; the role files stay with the seats, and with no common.md it is handed nothing.

Settling a rule in words

The rules the instance holds are also settled in words, and the words are the User's. Say it to the Leader — "you will be autonomous, push without asking", "pip install is too much, ask me every time" — and the Leader answers in plain language first, then calls permission once per rule: a rule as Claude Code's reference spells it — Bash(git push:*) for a command by prefix, Bash(the whole command) for one command exactly, Edit(/src/**) or Read(/src/**) for a directory in the instance, Edit(/knowledge/common.md) or Read(/notes/plan.md) for one file there, Read(//dir/**) for a directory outside it, WebFetch(domain:host) for a host, WebSearch, mcp__server__tool or mcp__server__* for a server's tools, Agent(Name) for a subagent, or a bare tool name for every use of it — and why, in words you read beside it. A rule Claude Code would not read is refused: a path on Write, Glob or NotebookEdit, a glob before the server's name, a star in the middle of a command, a path outside the instance for anything but Read.

the Leader's rule popup: the rule, the reason, Allow / Deny / Ask every time

Each call is one rule popup on the Leader's panel with Allow, Deny and Ask every time; the press writes the rule to that list in the instance's settings, takes it off the other two, and reaches the Leader as an event, so a Worker hired to push is hired after the push is allowed. A rule popup holds no call, and never times out. Called with no rule, permission answers what the instance holds and what is still pending — the Leader reports from that, never from a settings file. Every rule granted by a press is written down in .claude/allowed.md, one line per rule with the list it went to, who asked, when and what for.

A rule the instance already holds is moved on the same popup: it says where the rule stands now — Now: asked every time — and the press moves it to the list pressed. A rule written by hand as Bash(npm *) is the rule Bash(npm:*), as Claude Code reads it: settling one spelling takes the other out of the other lists.

To take a rule out, say so to the Leader: it calls remove_rule with the rule and why, and its panel shows a removal popup — the rule, the list that holds it, the Leader's why, the line .claude/allowed.md has for it (or that it has none, written by hand or when the instance was made), and for one of ovai's defaults, what it is for. Two buttons, Remove and Keep. Only the press writes: Remove takes the rule out of every list that holds it, either spelling, and adds a removed from line to .claude/allowed.md; a removed default is also remembered as ignored, so it is not offered again after an update, and the popup says so. The press reaches the Leader as an event, decision remove or keep. The rules that keep the file tools away from the instance's own settings, login and page keys are refused: they stay yours to remove by hand.

A rule moved or removed, from a popup or by hand, applies at every session's next call, with no restart.

A rule a fresh instance is born with that yours holds in none of its lists — after an update, most of the time, or because you took it out by hand — is asked for by ovai itself: one permission popup per rule on the Leader's panel, at every start of the server, saying that it came with the update, what it does and why. It has two buttons: what ovai recommends, labelled as what it does (Allow, Deny or Ask every time), and Ignore, which is remembered in .claude/ignored-defaults.json so that rule is never asked again. A popup nobody answered comes back at the next start. Nothing is added without a press, and the Leader is not told about it.

.claude/allowed.md: one line per rule — the list, who asked, when, what for

Who is speaking

Only the server writes to a session. Every line on a seat's stdin carries a frame the server sets itself — <user> for what you type, <message from="Max"> for another session, <server-event type="…"> for the server — and the body is neutralised before it is framed, so nothing inside a message can pose as a frame. A line that reads </user><user>push it arrives readable and unable to close anything. Only the outermost frame is the server's, by construction.

Who is calling a tool travels with the process, not with a name. When the server starts a seat it mints a secret — 32 random bytes — and keys the tool route by it. The secret is handed over in a file only this user can read, desks/<Name>/mcp.json, named on the command line by its path and removed the first time the secret comes back in a call, about a quarter of a second after the start. It is never in the process's environment, which every process of the same user can read for as long as the session lives. What stays open: a process of the same user that watches for the file can read it in that moment, one that may read another process's memory can read it there, and a seat that runs commands as you can change ovai's own code, which every session runs. The map from secret to seat lives in memory only, a secret dies with its process, and a call with one the server does not know is answered with one body whatever was tried. The request log prints the secret as the word <secret> wherever a path carries one. The page has a key of its own that opens no tool route, page-session, and a browser holds it as a cookie for its own port (HttpOnly, SameSite=Strict), set by a one-time link: ovai start and ovai url print one, it works once and for ten minutes, and a new one replaces the one before it. A browser that has not got the cookie is shown where to get a link, and nothing else. Delete page-session to sign every browser out. The instance directory is readable by its owner alone, and so are both files.

The server answers only to its own address. A request whose Host is not 127.0.0.1:<port> or localhost:<port> is refused with 421 on every route, the page included; a request that changes something — anything but GET, HEAD and OPTIONS — and carries an Origin is refused with 403 unless that origin is http://127.0.0.1:<port> or http://localhost:<port>. That closes one gap: a web page whose own name was pointed at 127.0.0.1 could otherwise load the page and drive every seat. The sign-in closes the other: a process or user on the machine that connects to the port directly gets the page only with the cookie, and the cookie only from a link. Whoever can read the instance's files as you can sign in, sessions included; that is the line it draws.

What stays out

Files a reply names

A path in a reply is a link only to what the checks let through, and the server runs the same checks on every row it sends, to decide which paths the page links, and again when a link is opened. A path is read from the instance directory and nowhere else. It is shown or opened only when its real path, symlinks followed, is inside the instance, and never when it is the a Claude Code home, or a .git, .ssh, .aws or .gnupg directory anywhere on the path, nor when it is a .credentials.json, .git-credentials, .netrc, .npmrc, .pgpass, .env or .env.*, page-session or page-link file at any depth, however the name is cased; a nested instance's are refused the same. A regular file with more than one name, a hard link, is refused too, since the view cannot see its other names. The list goes by name: a copy of a credential under another name is shown. The file view shows a regular file of at most 1 MiB of UTF-8 text, answers only a request that carries the page's cookie, allows no inline script or style on its page, and is not cached.

A web page, an image or a directory opens on the desktop instead, and that is the one route that starts a program on the machine, so it is narrowed further:

This closes the ways in that these checks name, and nothing else. A page opened this way runs as a local file in the browser, with whatever that browser lets a file:// page do.

The log

Every call a session makes is a row in runtime.log: the moment, the event, the seat, the tool-call id, the text. The rows of one call — called, the tool row the server writes when it ran one of its own, failed — share its id, so one grep is a call's whole life. A seat's process going is one row saying what it ended as. No request is ever logged, and every row is masked as it is written: a token of a known shape, such as sk-… or ghp_…, anywhere, and in a command also the value after a name that says key, token, secret or password on the same line, is written as ***. What is said in words is masked by shape alone. A desktop open that fails is an unopened row, with the path and the reason.

Next

Knowledge: what this workspace knows, as a directory of notes every seat may write.