---
name: spoiler-setup
description: Set up Spoiler in the current product repository. Install the CLI, build a feature map from the source code, and read the first PostHog session recordings.
---

# Set up Spoiler

Spoiler reads session recordings and reports what each user was trying to do, how it ended,
and what got in the way. You are setting it up in the user's product repository.

Work through the steps in order. Find what you can in the repository yourself, ask the user
only for what you can't, and ask everything you need in one message. The full documentation
is indexed at https://spoiler.sh/llms.txt, and every page is also available as Markdown.

## Rules

- Credentials come only from the environment: `POSTHOG_API_KEY` and `OPENROUTER_API_KEY`.
  Never print them, and never write them to a file.
- Never choose a model for the user. Ask which OpenRouter model id to use.
- Recordings hold what users saw and typed. Before the first model call, show the user what
  will be sent and wait for their go-ahead.
- Each command writes one JSON artifact to `--out` (or stdout). Failures are JSON on stderr:
  `{"error": "...", "retryable": false}`. Exit code `75` is a transient upstream failure
  (rate limit, timeout): wait, then retry. `spoiler <command> --help` lists every flag.

## 1. Install the CLI

```sh
command -v spoiler && spoiler --version
```

If it is missing, install it with one of:

- `curl -fsSL https://spoiler.sh/install | sh` (macOS and Linux; installs to `~/.local/bin`)
- `uv tool install spoiler`, or `pip install spoiler`
- `cargo install spoiler --locked` (Rust 1.88 or later)

If `~/.local/bin` is not on `PATH`, tell the user how to add it.

## 2. Ask for what the repository can't tell you

Look first: the product's host name and who uses it are usually in the repository (deploy
config, README, marketing copy). Then ask the user, in one message, for:

- The PostHog project id, and the PostHog host: `https://eu.posthog.com` (the default),
  `https://us.posthog.com`, or their self-hosted URL.
- The OpenRouter model id to use.
- Confirmation of the host name and audience you found, for example
  `app.example.com` and "workspace admins".

Check that both keys are set without printing them:

```sh
test -n "$POSTHOG_API_KEY" && echo "PostHog key set"
test -n "$OPENROUTER_API_KEY" && echo "OpenRouter key set"
```

`POSTHOG_API_KEY` is a PostHog personal API key with the `query:read` and
`session_recording:read` scopes. If a key is missing, ask the user to export it in the shell
you run commands in.

## 3. Write the product config

Create `spoiler/product.json`, with one entry per app recorded in PostHog:

```json
{
  "apps": {
    "web": { "project": 12345, "host": "app.example.com", "audience": "workspace admins" }
  }
}
```

The app id (`web` here) is yours to choose; later commands take it as `--app web`. A native
iOS or Android app recorded in PostHog is a separate entry.

## 4. Choose the source files

This is where you help most. `vocab build` sends only the files you name to the model, which
names the product's pages, controls and rules from them, citing each by file name and line.
Pick the files that define what users see and do:

- The route table or page entry points, so every page gets a name.
- Components with the main controls: buttons, forms, menus, dialogs. Controls with
  `data-testid`, `aria-label` or visible labels give the most reliable matches.
- Server code holding the product's rules and the error messages users see: limits,
  permissions, billing, validation.

Start with the files behind the product's most important flows; every file goes to the model
in one request, so keep the set focused. Skip tests, styles, generated code and vendored
libraries. Sources must be UTF-8 text. Citations use the file name only, so avoid two sources
with the same name.

Tell the user which files you chose and why.

## 5. Build the feature map

```sh
spoiler vocab build \
  --config spoiler/product.json \
  --source app/routes.ts \
  --source app/routes/members.tsx \
  --source app/members.server.ts \
  --source-revision "$(git rev-parse HEAD)" \
  --model "$MODEL" \
  --out spoiler/vocab.json

spoiler vocab check --vocab spoiler/vocab.json
```

Then review it with the user:

```sh
# What was mapped.
jq '.vocabulary | {surfaces: (.surfaces | length), features: (.features | length), terms: (.terms | length)}' spoiler/vocab.json
# What the sources left open.
jq '.vocabulary.gaps' spoiler/vocab.json
```

- `gaps` lists what the sources didn't cover. If a gap matters, add the file that covers it
  and build again.
- `vocab check` reports `warnings`: entries that can never match. Fix or remove them.
- The model's usage and cost are in `.provenance.generator.usage`.

To correct entries by hand, edit the vocabulary and package it again, with the same sources
and no model call:

```sh
jq .vocabulary spoiler/vocab.json > spoiler/vocab.candidate.json
# edit spoiler/vocab.candidate.json
spoiler vocab build \
  --config spoiler/product.json \
  --source app/routes.ts \
  --source app/routes/members.tsx \
  --source app/members.server.ts \
  --candidate spoiler/vocab.candidate.json \
  --out spoiler/vocab.json
```

## 6. Find recordings

```sh
spoiler recordings list \
  --project 12345 \
  --since 2026-09-20 \
  --until 2026-09-27 \
  --limit 20 \
  --out spoiler/page.json

jq -r '.recordings[] | [.session_id, .start_time, .active_s, .clicks, .first_url] | @tsv' spoiler/page.json
```

- Only recordings that started more than 24 hours ago are listed; choose a window that has
  settled. For US or self-hosted PostHog, add `--host`.
- Prefer sessions with clicks and some active time.
- More recordings: pass the page's `next_cursor` back as `--cursor`.

## 7. Read one session

Fetch the recording once, then prepare the model request without sending anything:

```sh
mkdir -p spoiler/recordings spoiler/sessions
spoiler recordings fetch --project 12345 --session "$SESSION" --out "spoiler/recordings/$SESSION.json"

spoiler run \
  --recording "spoiler/recordings/$SESSION.json" \
  --vocab spoiler/vocab.json \
  --app web \
  --prepare-only \
  --out "spoiler/sessions/$SESSION.request.json"
```

Show the user what would be sent. It is the trace, one line per action, and the parts of the
feature map it touched; the recording itself is never sent:

```sh
jq -r '.visits[].request.messages[] | "--- \(.role)\n\(.content)"' "spoiler/sessions/$SESSION.request.json"
```

When they agree, run it with the model:

```sh
spoiler run \
  --recording "spoiler/recordings/$SESSION.json" \
  --vocab spoiler/vocab.json \
  --app web \
  --model "$MODEL" \
  --out "spoiler/sessions/$SESSION.json"
```

`run` makes one model call per visit with user actions. `--visit N` reads just one.

## 8. Report back

```sh
# Each task: how it ended, what the user wanted, what stopped them.
jq -r '.visits[].analysis.summary.tasks[] | [.outcome, .goal, (.obstacle // "")] | @tsv' "spoiler/sessions/$SESSION.json"
# Each problem: its kind and severity, what happened, and the likely reason.
jq -r '.visits[].analysis.summary.friction[] | [.kind, .severity, .what, (.why // "")] | @tsv' "spoiler/sessions/$SESSION.json"
# Claims the check dropped or couldn't verify.
jq '.visits[].analysis.check' "spoiler/sessions/$SESSION.json"
# What the model calls cost.
jq '[.visits[].analysis.model.cost_usd] | add' "spoiler/sessions/$SESSION.json"
```

Outcomes are `done`, `workaround`, `gave_up` or `unclear`. Summarize the tasks and problems
for the user in plain language, and cite the trace refs (`e3`, `e5`) behind each.

## 9. Leave it tidy

- Add `spoiler/recordings/`, `spoiler/sessions/` and `spoiler/page.json` to `.gitignore`: they
  hold user data. `spoiler/product.json` and `spoiler/vocab.json` can be committed.
- Rebuild the feature map when the chosen sources change. Each trace records the feature map
  it was compiled against.
- Offer next steps: read more sessions from the same window, or the docs at
  https://spoiler.sh/docs.
