# The away view, and the switches in front of it

> One page your own machine serves to a phone you paired at it, what it took to make acting safe, and every setting you have to find to turn any of it on.

Published 2026-10-10 · by The Tade project · tagged away, safety, setup.
Published at https://tade.sh/blog/the-away-view-and-the-switches-in-front-of-it/ — part of https://tade.sh/blog/.

---

Tade is a terminal program with no daemon, no account and no cloud component,
and the thing people ask for anyway is to see it from the sofa. So there is one
listener now, and it is the only one there has ever been: a page the window
serves from your own machine to a device you paired by scanning a code in it.

**It is built, tested, and not in the published package**: `tade-sh` 0.1.1 was
packed on 2 October and the `v0.1.1` tag carries 1,146 files, not one of which
is under `packages/web/`. Until a release carries it, the way to it is the
`source` rows at the foot of [Install](/#install).

This is the setup, in the order somebody actually does it, with the parts that
have no command said plainly rather than left out.

## 1. Turn it on

```sh
tade web on          # loopback only: http://localhost:7654/
tade web status      # what is set, what is listening, who is paired
tade web off         # the setting, and every credential
```

`tade web on` writes one setting and tells you it **takes effect the next time
Tade starts**, which is honest rather than half-done: the listener comes up
with the window, and the epoch a connected phone holds is per server start, so
turning it on mid-session would mean tearing down and rebuilding something
somebody is looking at.

`tade web off` is two acts under one word. Off that left credentials behind is
not off — a setting somebody turns back on next week would otherwise let every
phone that was ever paired straight back in.

## 2. Decide who can reach it

> **A screen from Tade — `away-on-the-network`.** The same panel with the away view bound to every interface: what anybody on the same wifi can read is said above the code, and the one command that gives a phone HTTPS while Tade keeps listening on this machine alone is in the same breath.
>
> The same panel with the away view bound to every interface: what anybody on the
> same wifi can read, said above the code, with the way out in the same breath.

`loopback` and `lan` are a second decision and not a third option on the first
one, because *on* and *on the network* being one control is how a laptop in a
café ends up serving its control room to the café. The sentence above the code
is the whole of what a person deciding needs:

> On your network this is plain HTTP: anybody on the same wifi can read every
> page it serves and the session cookie with it. `tailscale serve
> localhost:7654` gives a phone HTTPS while Tade keeps listening on this
> machine alone, which is a smaller surface than this one.

A LAN bind is also a **read-only transport**, and that is enforced rather than
advised: a credential that crossed a network in the clear never buys an act,
whatever is turned on later, and that is re-asked at the moment of every act
rather than decided once at pairing.

If you use a proxy, its name goes in `surfaces.web.trusted_hosts`. **No
forwarded header is ever read.** A deliberate proxy is trusted by being named
by a person; a header is trusted by whoever sent it.

## 3. Pair a device, at the machine

> **A screen from Tade — `away-pairing`.** The away view in the window, on and on this machine alone: the two addresses it is bound to, a QR code to scan a phone in with, and — under it, where somebody is deciding — that an agent on this machine could pair itself, with Disconnect everything beside it.
>
> The away view in the window: the addresses it is bound to, a code to scan, and
> under it that an agent on this machine could pair itself, with Disconnect
> everything beside it.

There is one way in and it is the panel above: press its key for a code, scan
it with the phone's camera, and answer the question the window asks you.

`tade web pair` cannot mint a code and says so, with exit 1. A pairing ticket
lives in the memory of the process whose server has to claim it, and there is
no channel from a shell into the window. A typed URL cannot pair either — the
ticket is in the code's fragment — so somebody who types the address is told to
scan. **A six-digit typeable code is not built**: it is a second kind of
credential with its own threat model, and it is outstanding rather than decided
against.

The ticket is burned on the **claim**, before anybody at the machine is asked.
That is the one ordering where a replay, a concurrent attempt, a refusal, a
deadline and a thrown error all find nothing.

The sentence under the code in that panel is the one nobody wants to write:

> an agent on this machine could pair itself

`0600` keeps the config and the device list from other people, and an agent is
not another person — agents run as you, and nothing sandboxes them. What stands
against it is that the panel lists **every** device there is, every act is
journalled under the device that took it, and *Disconnect everything* needs no
network at all.

## 4. Decide what a device may read

A paired device gets names, counts and states, and nothing anybody wrote. That
is the default and it is not a degraded one: a page granted nothing still
answers the question the whole surface exists for — *is anything waiting for
me*.

Everything else is a grant, made at the machine, per device. Eleven of them:
`titles`, `intent`, `notes`, `findings`, `spend`, `reviews`, `accounts`,
`talk`, `requests`, `material`, `workflows`. Two devices with two scopes really
are two projections built per device rather than one filtered afterwards, and
the project half of a task id is the boundary — so *no project* is never read
as *every project*.

The last two of those are the ones to think about, because they carry text a
stranger wrote: `requests` is what each outside request calls itself, and
`material` is the request as it arrived. Neither is scrubbed, for the reason a
note is not — rewording somebody's request is the same lie as rewording a
note — and what stands instead is the grant, a budget, the heading that travels
with the words, and a page that builds no markup at all: no `innerHTML`, no
Markdown renderer, no `unsafe-inline`.

## 5. Decide what a device may do — and this is not a terminal step

Reading is the default. Acting is a third decision, and it is the one with no
command-line writer at all.

```yaml
# ~/.tade/config.yaml
surfaces:
  web:
    enabled: true
    acting: true                 # the eight verbs
    orchestrator: false          # talking to Tade: a fourth decision
    drafts: false                # saving a draft workflow: a fifth
    install: false               # a shell on the phone, and three keys under it
    trusted_hosts: ['studio.ts.net']
```

`tade web on` and `tade web off` write `enabled`, and `--lan` writes `bind`.
The other ten keys in that group have no command at all: `tade config` reads
(`--json`, `--check`) and does not write, **there is no `tade config set`** and
no `tade settings`. They are set on the window's own Settings page, by telling the
orchestrator, or in a text editor. That is defensible — these are the keys that
widen who can reach the control room, and the rule in this repository is that
the thing granting authority is a person's act — but *Settings page or text
editor* is not something a setup document can put in a command block, so it is
a paragraph instead.

With `acting` on, and then only for a device you widened in the away panel, and
then only over this machine itself or an https name you named, there are eight
verbs:

| verb | what it does |
| --- | --- |
| `answer` | allow or deny what an agent is already waiting on |
| `steer` | say something to an agent that is already running |
| `queue` | pause, resume, reorder or let queued work start |
| `park` | set a task aside, or pick it back up |
| `note` | write something down about a task, word for word |
| `context` | add a line to what a task's agent is told |
| `done` | mark work finished, with two presses and a literal confirm |
| `intake` | approve a request a grant here already allowed |

And nothing else. Not a setting, not a credential, not a command, not a new
agent, not a push, not a merge, not a check overruled, not a review answered,
not a workflow published, not a grant given to any device. Those are not flags
that happen to be off: with `acting` off the acting routes are **not in the
route table at all**, so a crafted call is the same `404` as a path nobody
built, and the things in the paragraph above have no route in any
configuration.

Three details are load-bearing:

- **`steer` cannot start an agent.** It is a message into a conversation that
  is already running, and where no agent is, it is refused.
- **`done` needs two presses and a literal `confirm: true` in the body**, and
  no other verb stands in for it.
- **A verb is a target plus the state it expects.** An idempotency key is a
  fast path and never the guarantee: what makes a replayed request safe after a
  restart is that the act names what it assumed and the window re-checks it at
  the moment of acting.

Talking to Tade is a fourth switch and deliberately not implied by acting. A
verb names a target and the state it expects; a message is free text to a model
that holds tools. While Tade is answering one, its own tools are narrowed to
reading that device's own view of your work plus whatever that device was
granted, and every other tool it has is refused in code — including the door
that changes a setting, which is not reachable from a remote turn **at all**.
That last part is what defeats the obvious attack: a message that names a
setting you happened to name last week.

## 6. A shell on the phone, if you want one

Installing is a sixth switch, and the first whose effect is on somebody else's
hardware. Four things about it are worth knowing before turning it on, and
none of them is discoverable by trying it.

**It needs a secure origin.** `localhost` is one; a private address on your own
wifi is not, whatever it is bound to, and the browser refuses without
explaining itself. `tailscale serve localhost:7654`, or a reverse proxy whose
name is in `trusted_hosts`, is what gives a phone one — **and the device pairs
again there**, because a session is bound to the exact host it was minted on and
nothing is carried between origins.

**What it may keep on its own disk is counts.** How many are working, how many
want you, how many are queued, and the moment they were true. No title, no
intent, no note, no request, no name, and nothing can be acted on from it. It
is still a copy on a phone: signing that device out clears it the next time
that device reaches this machine, and what was already downloaded cannot be
taken back.

**A notification says how many and nothing else**, unless you turn details on.
Every one of them travels through the push service that phone's browser chose —
Apple's, Google's, Mozilla's — which sees that something was sent to that
subscription, when, and how big it was, and cannot read what it says. They are
sent by the window on its own beat, so a closed window and a sleeping machine
send nothing, and **nothing is caught up**: a window that comes back says what
is true now rather than six things about last night.

**Turning installing off is something the device has to be told.** A `404` on a
worker's script does not remove it — the service worker specification was asked
to unregister on 404 and 410 and decided against it in 2017, because a bad
afternoon at a data centre would otherwise unregister everybody. So what is
served instead is a worker that deletes every cache of Tade's and removes
itself, the next time that device reaches this machine. A device that never
comes back keeps what it had, and what it had is the page: no project, no task,
no note, no figure, and no way to read one without a session.

One thing this post will not tell you is that any of that has been read by a
real phone. The served bytes are driven through install, upgrade, rollback and
uninstall by a test, the committed icons are decoded, and the offline
allow-list is asserted — and no notification has ever been delivered to a
device from this repository. That is the difference between a tested surface
and a field-tested one, and the second one is somebody's afternoon with a phone
rather than another test.

## The thing that is true however many switches are on

> **A screen from Tade — `away-pairing`.** The away view in the window, on and on this machine alone: the two addresses it is bound to, a QR code to scan a phone in with, and — under it, where somebody is deciding — that an agent on this machine could pair itself, with Disconnect everything beside it.
>
> The panel again: on, on this machine alone, with every device already paired
> and what each one may read.

The away view answers only while the window is open. Close Tade and nothing is
listening, which is also why closing Tade is harmless: there is no relay, no
tunnel and no queue for later anywhere in it, and a bookmark tapped with Tade
closed is the browser's own connection error rather than an empty control room.

A sleeping laptop serves nothing, and the page never pretends otherwise. When a
look fails, rows keep their state, their age freezes at the moment it was last
true, nothing becomes zero, and the page says what it actually knows:

> unreachable since 11:07 — the machine may be asleep, or Tade may be closed

Which is a third answer, and the reason it is worth having: an empty list and a
machine that has gone quiet are two different things to do next.

If the thing you want from a phone is to approve the work that arrived while
you were out, [the door it arrived
through](/blog/when-somebody-else-files-the-ticket/) is the other half of this.
