Build1 publisher2 min readPublished
The weaker of agent-browser's two safety flags is the one that survives a restart
An end-to-end read of the browser-automation CLI found --pin-tab written to an fsynced sidecar file and re-read at startup, while --allowed-domains is seeded only from an environment variable that a reconnecting command never sets.
The Engineer · Build desk

What happened
- A user report that a containment flag disappears silently when the daemon restarts prompted an end-to-end read of agent-browser, a browser-automation CLI built as a client plus a long-lived per-session daemon.
- Inside a live daemon, --allowed-domains behaves as documented: a later command that omits the flag inherits the adopted filter, and a launch that fails puts the previous filter back.
- The relaunched browser loses the WebRTC restriction as well, because restrict_webrtc is derived from the allowlist and the list is hashed into the launch hash.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure Reconnecting is the dangerous operation here: the session that comes back accepts any domain, and the check that refused a --state replay while a filter was active no longer fires.
- constraint A caller cannot verify containment before it starts driving the browser, because the active allowlist is not among the fields the session-info handler returns.
- cost Closing the gap costs one sidecar write and one load call at state construction, both already written in the same repository for the tab pin.
- decision Anyone shipping a flag whose documented semantics are sticky has to choose where the baseline is stored, because a suite that mutates the flag in a live process will pass either way.
Run any command against a session whose daemon has exited, and the client spawns a replacement. The client exports AGENT_BROWSER_ALLOWED_DOMAINS only for the command doing the spawning, and only when that command carried the flag [5]. DaemonState::new reads the filter from that variable and nothing else, at actions.rs:696-701 [4]. A reconnect that does not repeat the flag therefore builds a daemon whose filter is None [8].
That leaves the files on disk. {session}.config is a fingerprint of other daemon options, and allowed_domains is not among the hashed fields [6]. The five remaining sidecars, .engine, .provider, .extensions, .port and .stream, are deleted at startup [7][17]. Whatever the previous filter was, the reconnect path cannot read it back [18].
The allowlist is not the only thing that goes. ensure_state_replay_supported_by_active_domain_filter refuses --state and storageState while a filter is active, and with None it permits them again [9]. restrict_webrtc is derived from the same list, and the list is hashed into the launch hash, so the relaunched browser comes back without the WebRTC restriction [10].
The repository already contains the fix, in the file for the other flag. --pin-tab is the weaker boundary, and it was made restart-proof with a sidecar file [11]. The atomic writes and the fsync went to the tab pin. The header comment on cli/src/native/tab_binding.rs says that once a session is created with --pin-tab, subsequent commands and daemon restarts keep the strict semantics without repeating the flag, and that because a lost or corrupt binding silently drops that safety boundary, writes are atomic (temp file + rename), owner-only, and fsynced, with both save and load reporting failures instead of swallowing them [12]. tab_binding::load runs when the daemon state is built, at actions.rs:683, and --allowed-domains has no equivalent load [13].
Tests do not catch this because every end-to-end test of the domain filter mutates the filter [15]. A test that changes the allowlist inside a live process exercises the half that works. The handler a client would poll reports session, engine, launch hash and the whole restore-* family, but not the active allowlist [14]. The symptom in the original report was exit code 0 and nothing printed [16].
This transfers to your own CLI if it has a per-session daemon, a flag documented as sticky until explicitly dropped, and a cold-start constructor that reads the boundary from somewhere only one command writes. Where does the state constructor get the boundary on a cold start, and can any command other than the spawning one set it? The post says it includes a five-question procedure for any daemon-shaped CLI; the published text breaks off before the questions [19].
What to watch
- Whether agent-browser adds a tab_binding-style load for the allowlist, or extends the {session}.config hash to include allowed_domains.
- Whether the session-info handler starts returning the active allowlist so a client can detect a dropped filter.
- Whether the state-replay guard is changed to refuse when no filter has been recorded, instead of only when one is active.