# The daemon said yes to an option that did not exist

URL: https://tuios.dev/blog/the-daemon-said-yes-to-an-option-that-did-not-exist

> Driving the tuios control socket like a machine found a command that lost its spaces, a dropped parameter, and 88 settings of which six applied live.

tuios has a [control socket](https://tuios.dev/docs/control-protocol): a JSON protocol a program can speak to the daemon
to open windows, send input, read screens, change settings. It exists so that
agents and scripts can drive a session without faking keystrokes at a TUI.
Every verb had tests. The tests were green. Then I sat down to write the
documentation that teaches an agent to use it, and did the one thing the
tests had never done. I ran the documented examples over a real socket
against a real daemon, and looked at what actually happened.

Four findings, in order of how much they changed my mind about what a
passing test means.

## The command that lost its spaces

`send-keys` takes a token string in the tmux style: key names and text,
comma-separated. So the obvious way to run a command is:

```
tuios send-keys "echo hello,Enter"
```

The pane receives `echohello`, followed by Enter. The tokenizer aliases
commas to spaces and then splits on whitespace, so `echo`, `hello` and
`Enter` are three tokens, each sent as its content, with the separators
dropped. That is correct for a key-token language, where the separator is
punctuation rather than payload. It is also a trap for exactly the caller this
surface exists to serve: a machine that composed a shell command and reached
for the verb with "keys" in the name. Nothing warns you. The call exits zero,
and the pane runs a command that does not exist.

*[An interactive figure goes here. Open the page to use it.](https://tuios.dev/blog/the-daemon-said-yes-to-an-option-that-did-not-exist)*

This one is not fixed in code, because the tokenizer behaves as specified.
The fix is in the surface. The [skill documentation](https://tuios.dev/docs/agents#letting-an-agent-drive-tuios) now shows the failure
inline, annotated with exactly what gets typed, and steers text, including
whole shell commands, toward [`send-text`](https://tuios.dev/docs/cli-reference#send-text), which sends its argument verbatim.
The examples in that documentation are held by a test that parses every one
of them against the real command tree, so the docs cannot drift from the
binary.

## The parameter that fell on the floor

`new-window` takes a session and a name. I asked it for a workspace too:

```json
{"verb": "new-window", "params": {"session": "dev", "workspace": 2}}
```

A window came back: created, named, success envelope, everything in order.
On workspace 1. Dropping an unknown field is what `encoding/json` does by
default, and it is the worst answer available to a machine caller. The call
did less than it was asked, reported success, and left no trace of the
difference. A human notices the window opened in the wrong place. An agent
reads `"type": "window_created"` and moves on, wrong about the world from
then on.

The fix has two halves. `workspace` became real, along with `cwd` and
`focus`. And the protocol layer now checks every incoming parameter against
the verb's published schema before the handler runs. Anything unrecognised is
refused, with the closest match and the full list of accepted parameters.
The refusal is useful beyond typos. A parameter the verb does not take yet is
exactly what a caller built against a newer tuios sends to an older one, and a
refusal is the only honest answer in that situation. Turning the check on
also caught two parameters that were real but missing from their verbs'
declared schemas, which would have become unreachable the moment enforcement
landed. The declared surface and the implemented surface had already drifted
apart. Nothing was comparing them.

This is the same kind of request against the daemon today, with the
workspace parameter misspelt:

*[An interactive figure goes here. Open the page to use it.](https://tuios.dev/blog/the-daemon-said-yes-to-an-option-that-did-not-exist)*

The full list of error codes is in the [protocol
reference](https://tuios.dev/docs/control-protocol#error-codes).

## The option that did not exist

```json
{"verb": "set-option", "params": {"key": "appearance.totally_made_up", "value": "whatever"}}
```

This came back `option_set`. Not an error. The response did include
`"applied": false`, which sounds like a signal until you learn it was one bit
meaning two things: "no client is attached to apply this right now" and
"that key means nothing and never will". A caller cannot tell a setting that
will take effect on next attach from a typo. Both were reported as success.

Now the path is resolved against the option registry first, and a miss is an
error, `option_not_found`, carrying the closest match and the complete list
of valid paths. The value is validated too, by trying the assignment on a
throwaway default config before touching the session, so the check and the
apply cannot disagree about what is assignable. And when `applied` is false,
the response says why. Both requests from this section, sent to a headless
session today:

*[An interactive figure goes here. Open the page to use it.](https://tuios.dev/blog/the-daemon-said-yes-to-an-option-that-did-not-exist)*

## Six paths out of 88

The last one was the quietest and the largest. Applying a setting to a
running tuios went through a hand-written switch, and the switch knew six
paths, all of them appearance options: border style, dock position,
animations, three flavours of window button. The option registry at that
commit declared 88 (the full list today is one [`tuios
list-options`](https://tuios.dev/docs/cli-reference#list-options) away). Everything else, most of the sidebar's settings and all
but one of the dock's, could be written into the config file, read at
startup, listed by the CLI, accepted by `set-option`, and would do nothing
whatsoever to the running program. Not rejected. Accepted, recorded,
inert.

So the whole sidebar could be configured over the socket, verb by verb,
success by success, without a single visible consequence. The fix deleted the
dead end rather than extending the switch. Every path now goes through the
same route a config file load uses: one assignment function driven by the
registry, then the same live-apply step. The registry itself is held to the
config struct by a reflection test that walks every scalar field and fails in
both directions: a field without a registry entry, or an entry without a
field. The number 88 can never again be quietly larger than the number six,
because there is no six.

## Why every test was green

The tests were not thin. They were pointed at the wrong layer.

The end-to-end suite really did drive `set-option` over the socket, and here
is the assertion it made: set `mouse` to `on`, get `mouse` back, expect
`on`. `mouse` is not a tuios option. The test proved the daemon's key-value
store round-trips a string, which it does beautifully, and proved nothing
about any option existing or taking effect. It now sets a real registry path
and asserts the value came back from the session, not from defaults.

The unit tests called verb handlers as functions, with a fake client wired to
answer every command with a hardcoded success. A handler tested like that
cannot fail for any of the four reasons above. It cannot even fail for not
being registered. The commit that replaced them says it plainly: a handler
test would pass for a verb that was never registered, and registration is
half of what these verbs are.

The replacement suite starts a real daemon, connects over the socket, sends
the verb as bytes, and asserts on the response and then on the state the
daemon holds afterwards. It is slower, and it is the only kind of test on
this surface whose passing means what it appears to mean.

## What I keep from this

An API for machines fails differently from a UI for people. A person who asks
for a window on workspace 2 and watches it open on workspace 1 has already
noticed: the interface reports itself through their eyes. A program has only
the response envelope, so every gap between what was reported and what was
done becomes a false belief in the caller, compounding silently. For this
kind of surface, "accepted and ignored" is strictly worse than any error. The
couple of hours I spent driving my own API the way its real callers would
found more product defects than the handler-level suite had found in its
whole lifetime. The features were not broken. They were never wired to
anything, and no green is as untrustworthy as the green of a test that cannot
reach the wire.

*Note, September 2026: an earlier version of this post showed the option
example as `tuios set-option ...`. There was no such CLI command: `set-option`
is the socket verb, and the CLI command at the time was `tuios set-config`.
It also said the skill steered whole commands toward `run-command`, which
runs tuios commands such as `ToggleZoom`, not shell commands. Both are
corrected above.*
