Markdown · llms.txt

Security

tweet-factory stores another person’s posts (everything you liked or bookmarked) plus your own, and it holds a long-lived X refresh token. Treat the checkout like a credential.

Secrets

  • .env, .tokens.json, and .cloud.json are gitignored.
  • .tokens.json and .cloud.json are written atomically, mode 0600.
  • When tf updates .env, and when setup_x.sh writes keys, the file is mode 0600. A hand-edited .env is not. Run chmod 600 .env after editing it yourself. .env.tmp and .tokens.json.tmp are gitignored.
  • Opening tweets.db for write sets it to mode 0600, including an existing file that was looser. backup.sh sets mode 0600 on each snapshot of tweets.db and persona/PERSONA.md.
  • Do not log token or key values. The docs’ verification commands print set or missing, not the secret. X token and API errors log the status code only.
  • backups/ contains the database and the persona. It is local on purpose. Do not point it at a public remote.

OAuth callback

tf auth listens on 127.0.0.1 for about five minutes, then stops. Set OAUTH_BIND_HOST to something else only when you must authorize from a different machine (for example a Tailscale address). The CLI prints a warning when the bind is not loopback. Register the exact OAUTH_REDIRECT_URI on the X app. State is checked; a mismatch aborts as a possible CSRF.

One refresh token, one machine

X rotates the refresh token when it is used. Two hosts running tf fetch against the same .tokens.json will invalidate each other. Symptoms: 401s, then tf auth required. Keep a single scheduler.

What is sent off the machine

The default build does not contact tweetfactory.sh. TF_LICENSE_KEY is not sent anywhere until a billing provider replaces resolveProvider() in src/entitlements/index.ts. That provider must not upload tweets.db, the persona, or OAuth tokens. See the hardening plan. tf login and tf sync are a separate opt-in. Login does not upload the database. Other network calls happen only to services you configured:

CallWhen
X APIauth, fetch, enrich of X articles
Your LLMtf generate, onboard generate. The default is the Claude Code CLI already logged in on the machine.
Tavilytf search, tf enrich, tf generate --timely, tf cron
Notiontf notion, cron sync
Slacktf notify, tf generate --post-to-slack, cron’s pending ping
tweetfactory.shtf login, tf sync push, pull, status, delete

Cloud sync encrypts tweet text, author descriptions, article extracts, Notion notes and page text, and persona and draft bodies before they are stored. The operator who holds SYNC_ENCRYPTION_KEY can decrypt them. Ids, usernames, metrics, tags, and filenames are stored in the clear so the account page can list files. Details: Cloud sync.

tf analyze, tf flush, and tf refresh do not call an LLM. tf refresh does call the X API. tf update downloads a public release binary and does not call the X API.

Scraped text

Tweets, article extracts, Tavily results, and Notion pages are untrusted. tf generate wraps them in <reference_data> and strips a closing fence that appears inside that text. The Claude Code subprocess is started with --tools "", --disallowedTools "*,mcp__*", --strict-mcp-config, --permission-mode dontAsk, and --max-turns 1. --allowedTools only pre-approves tools, so an empty allow list is not the control. Direct API backends have no tools in this CLI either; they get a single completion.

What this program will not do

It does not post, like, or bookmark on your behalf. The OAuth scopes are read plus offline.access. Publishing a draft is a separate action you take in the X client.

Verify. git status --ignored --short -- .env .tokens.json .cloud.json tweets.db shows those paths ignored (once they exist). stat -c '%a' .tokens.json is 600 after a successful auth. After tf login, stat -c '%a' .cloud.json is 600.