# Troubleshooting

## 401 or “token expired”

The access token was refreshed and rejected, or the refresh token was rotated by another machine. Run `tf auth` once on the machine you want to keep, click Authorize, and stop the other host’s cron.

`No .tokens.json` means auth has not succeeded in this directory. Check `--dir` and the working directory.

## Callback problems

`Timed out waiting for authorization (5 min)` — the browser never hit the listener. Re-run `tf auth`. Confirm the X app callback is exactly `http://127.0.0.1:8675/callback` unless you overrode the port and redirect URI.

`State mismatch` — the callback was not for this attempt. Re-run auth; don’t reuse an old URL.

`Token exchange failed` — the client id, secret, or redirect URI doesn’t match the app. The redirect URI in the exchange is the one the CLI printed (`Redirect URI in use`).

Headless: `tf auth --no-browser`, open the URL elsewhere, and make sure that browser can reach the bind address.

## 403 on a source

The log says `SKIP LIKES` or similar. Bookmarks in particular require OAuth 2.0 user context and the `bookmark.read` scope. Re-check app permissions (Read) and re-run `tf auth` so the new scopes are granted.

## Rate limit

A 429 sleeps until `x-rate-limit-reset` plus two seconds, then retries that page. Let it sit.

## `TAVILY_API_KEY not set`

`tf search`, `tf enrich`, and `tf cron` need the key. `tf cron` fails at the enrich step even if fetch already succeeded. Use `tf refresh` when you only want a refresh. `--timely` on generate is best-effort: a missing key or a Tavily error is ignored and drafts still run.

## `claude` CLI not found

Install Claude Code and log in, or set `TF_LLM_PROVIDER` to `anthropic`, `openrouter`, or `openai` and the matching key. A provider set without its key throws `API key is not set`.

## Generate returned prose or invalid JSON

The model has to return a single JSON object matching the bundle. Zod throws otherwise. Re-run `tf generate`, or set `TF_LLM_MODEL` / `--model` to a model that follows the JSON instruction. `--json` shows the failure on stdout/stderr without the markdown file.

## Notion pages missing

The integration can only see pages you shared with it. `tf notion search` is the check. `sync` logs `unreachable` and skips a page that was unshared later.

## Slack did nothing

Unset `SLACK_WEBHOOK_URL` is a logged skip, not a crash. Cron only pings when the pending count is greater than 0. The first flush baselines to 0, so the first cron after a backfill often stays quiet. Fetch something new, run `tf flush`, then cron again.

## `tf onboard` won’t open

The TUI uses OpenTUI’s native addon. Build with `bun run build` on the same OS you run (the release workflow compiles per target for this reason). A dumb terminal or a missing addon fails at launch. `--selftest` is a one-frame smoke check in an environment where the renderer can start.

## Bun missing

`./setup_x.sh` installs it under `$HOME/.bun/bin`. Open a new shell or export that path. `deploy.sh` logs a warning and skips the rebuild if `bun` is absent.

## Wrong project directory

Commands read whichever root they discovered. If counts look empty, you are probably not in the checkout that holds `tweets.db`. Pass `--dir` or `cd` to the directory that contains `.env`.

## Persona still sounds generic

`persona/PERSONA.md` is missing, still the template (`@YOUR_HANDLE`), or very thin. `tf analyze` shows what the model can see from the database. Fill the voice rules and banned phrases, then generate again.

> **Verify.** After a fix, re-run the failing command once. `./setup_x.sh --check` should list the required OAuth keys as present and Bun as installed.
