---
name: publish-through-a-browser-already-signed-in
description: Post to a platform that has no usable API by driving a browser a person signed into by hand, and check the result as a stranger rather than as the author.
runbook:
  schema_version: v1
  semver: 1.0.0
  profile: P1
  min_runtime_profile: R1
  domain: ci-cd
  targets:
    - linux
  trigger: manual
  execution: human-with-agent
  risk: reversible-write
  blast_radius: global
  duration: 5-30m
  capabilities:
    - cli:curl
  publisher: std
  lang: en
  license: CC-BY-4.0
  inputs:
    debug_port:
      type: string
      required: true
      description: the debugging port the signed-in browser is listening on
    profile_dir:
      type: string
      required: true
      description: the browser profile directory holding the session for this account
    published_url:
      type: string
      required: false
      description: the address the run reported, for checking from outside
  steps:
    - id: s1
      kind: check
      title: The signed-in browser is up and answering
      tool: cli:curl
      command: curl -sf http://127.0.0.1:{{inputs.debug_port}}/json/version
      expect: the browser answers with its version
      assert:
        exit_code: 0
      on_fail: s2
      next: s4
    - id: s2
      kind: action
      title: Start the browser by hand on its own profile
      risk: read-only
      expect: a window opens on the profile that holds this account's session
      on_fail: escalate:s11
      next: s3
    - id: s3
      kind: check
      title: The port answers now
      tool: cli:curl
      command: curl -sf http://127.0.0.1:{{inputs.debug_port}}/json/version
      expect: the browser answers
      assert:
        exit_code: 0
      on_fail: escalate:s11
      next: s4
    - id: s4
      kind: check
      title: The session in that profile is still signed in
      expect: the platform shows the account as signed in, not a login form
      on_fail: escalate:s11
      next: s5
    - id: s5
      kind: action
      title: Connect to the running browser, never launch one
      risk: read-only
      expect: the tool attaches to the browser that is already open
      on_fail: escalate:s11
      next: s6
    - id: s6
      kind: action
      title: Write the post as a draft
      risk: reversible-write
      expect: a draft exists on the platform with the whole body in it
      rollback_ref: s10
      on_fail: escalate:s11
      next: s7
    - id: s7
      kind: check
      title: The draft carries the whole body
      expect: the draft's length and last paragraph match what was sent
      on_fail: escalate:s11
      next: s8
    - id: s8
      kind: action
      title: A person presses publish
      risk: reversible-write
      requires_approval: true
      expect: the platform reports the post as published and returns its address
      rollback_ref: s10
      on_fail: escalate:s11
      next: s9
    - id: s9
      kind: check
      title: Open it from a profile signed into nothing
      expect: the whole post is visible — no paywall, no members-only wall, no login wall
      on_fail: escalate:s11
      next: end:success
    - id: s10
      kind: action
      title: Withdraw it, back to a draft
      risk: reversible-write
      expect: the address stops serving the post to anybody but its author
      next: end:aborted
    - id: s11
      kind: escalate
      title: Escalate to whoever owns the account
      to: team:platform
  source:
    url: https://runbooks.directory/r/std/publish-through-a-browser-already-signed-in/source.md
    fetched_at: 2026-09-04
    upstream_state: current
---

## When to reach for this

A platform that matters has no API worth using — or an API that cannot post what you
actually want to post — and the way to publish is the way a person does it: in a browser,
signed in. Automating that is ordinary. Getting it wrong quietly is also ordinary, and the
two failures below are the ones that cost the most.

## Connect to a browser; never launch one

The tool attaches to a browser a person started. It does not start one.

This is the whole arrangement, and it is not a preference. A browser the automation starts
is a browser with no session: it has never been signed in, and it cannot be signed in
without putting the account's password somewhere the automation can read. Attaching to one
a person opened means the credentials were typed by the person who owns them, once, into
the browser that holds them.

So the run's first question is whether that browser is up, and its first failure is a port
that does not answer. Ask the debugging port for its version before doing anything else —
it is one request, and it distinguishes "nobody started the browser" from every other
problem that would otherwise surface as a confusing timeout in the middle of a post.

## The profile directory is not a cache

The session lives in the browser's profile directory, and there is exactly one copy of it.
It is not reproducible: no build regenerates it, nothing in a repository contains it, and
losing it means every account signs in again by hand.

Profile directories are often kept somewhere that reads as disposable — a cache path, a
temporary directory, a dot-directory nobody documents. Anything that reclaims disk space by
looking at directory names will find it. Keep it somewhere named for what it is, and say in
the same breath that it holds the only copy of the logins.

Two mistakes worth naming, because both have happened. A directory whose name reads as
throwaway gets deleted during a clean-up — and the second is subtler: a profile inside a
container looks empty from outside, because the profile is in a hidden path and a plain
listing shows nothing. Look with hidden files shown before concluding a profile directory
is unused.

## Which port belongs to which account lives with the account

More than one account means more than one profile and more than one port. Where that
mapping lives matters: in the repository it is configuration that drifts from the machine
it describes, and the failure is publishing as the wrong person.

Keep it beside the account's own record — the same row that says who publishes and with
what — so that changing an account and changing its port are one edit rather than two, and
the second one cannot be forgotten.

## Check it as a stranger, because you are not one

This is the step that gets skipped, and it is the reason the procedure has a profile signed
into nothing.

**An author always sees their own post in full.** Paywalls, members-only gates and login
walls are invisible from the account that wrote the thing. Opening the published address in
the same browser proves only that the post exists, which was never in doubt.

So keep one profile deliberately signed into nothing, and open the result there. It is the
only way to see what a reader sees, and it costs one browser window.

## The last press is a person's

Where the platform allows a draft, stop at the draft and let a person publish it. Not
ceremony: the draft is where the formatting is checked, and formatting is what breaks — a
body pasted in pieces, a title taken from the wrong line, a code block that arrived as
prose. A draft that is wrong is a draft; a post that is wrong is a post somebody read.

## What an agent may do here, and where it stops

`execution: human-with-agent`. An agent can attach, write the draft and check it; the
publish carries `requires_approval` because it is the step that becomes public and the one
step here that no check performed beforehand can take back.

Several steps name no tool and the linter says so — starting a browser, seeing that a
session is still signed in, opening a page as a stranger. The warning is right and is left
standing: those steps are the human half of a human-with-agent procedure, a supervisor has
nothing to scope for them, and a record that suppressed the warning would be claiming
supervision it does not have. What it costs is trust, which is the correct price.
