# Bring your own subscription

> From getsimeon.com/docs/bring-your-own-subscription (27 October 2026). A write-up of how Simeon is built, for a coding agent (Claude Code, Codex, Cursor…) to use as a reference when building something like it. File paths are Simeon's own: take the ideas, not the paths.

## The idea in one paragraph

Simeon has no AI backend, API key or token bill. The student already pays for Claude (Pro/Max) or ChatGPT (Plus/Pro).
Both companies ship a **command-line agent** that runs on the user's own computer and signs in with that subscription:
`claude` (Claude Code) and `codex` (ChatGPT Codex). Simeon starts that program as a child process, hands it a prompt,
and reads what it prints. The agent does the model calls, using the user's own account and plan limits. Simeon is a
UI plus orchestration layered on top of a program the user already has.

```
 Simeon (SwiftUI app / study page in the browser)
        │  spawns a process: argv = prompt + flags, cwd = a chosen folder
        ▼
 claude -p "…"   or   codex exec "…"        ← the user's own install, signed in
        │  HTTPS, the user's own subscription
        ▼
 Anthropic / OpenAI
```

Nothing in the app calls an LLM API directly. The only contract it relies on is: **prompt in, text (or files) out.**

---

## 1. One file knows the difference: `Assistant.swift`

`local/mac-app/Sources/Assistant.swift` is the adapter. It is an enum with `.claude`, `.codex` and `.ollama`, and it is
the only place that knows each CLI's flags. Everything else asks it for arguments. The Windows/Tauri app mirrors it
in `desktop/src-tauri/src/assistant.rs`.

It handles five things:

| Job | Claude Code | Codex |
|---|---|---|
| **Find the binary** | known spots (`~/.local/bin`, Homebrew, the copy inside the Claude desktop app), then `command -v claude` in an interactive login shell (catches nvm/Volta/asdf) | same, `command -v codex` |
| **Signed in?** | `claude auth status --json` → `loggedIn` | `codex login status` |
| **Sign in** | writes a `.command` file running `claude auth login`, opens it in Terminal; the browser OAuth finishes it | same with `codex login` |
| **Install** | `curl -fsSL https://claude.ai/install.sh \| bash` (in Terminal, `InstallGuide.swift`) | `curl -fsSL https://chatgpt.com/codex/install.sh \| sh` |
| **Run** | `askArguments` / `buildArguments` (below) | same functions, different flags |

Two practical details:

- **PATH.** Apps launched from the Dock get a bare PATH, so `Shell.path` asks `zsh -lc 'echo $PATH'` once and puts the
  binary's own folder first. Without that, node/python3 aren't found.
- **Errors are read as text.** `Assistant.signedOut(...)` looks for phrases like "not logged in", "token has expired" or
  a 401, and `tooOld(...)` looks for "does not support this model". Each becomes a fix button (sign in again, update)
  instead of a raw error.

## 2. Two modes: *ask* and *build*

### Ask: a question in, plain text out, no tools

This covers Ask Simeon, the highlight-to-ask box and the home-screen chat.

```bash
# Claude Code
claude -p "<prompt>" --output-format text --no-session-persistence \
  --disallowedTools Bash Edit Write Glob Grep Task WebFetch WebSearch NotebookEdit Read

# Codex
codex exec "<prompt>" --sandbox read-only -c 'approval_policy="never"' --skip-git-repo-check [-i image.png]
```

- Claude Code can deny tools by name. Codex can't; its lever is the sandbox, and read-only still allows reading. So the
  process runs **in an empty folder** (`AskServer.askDir`): it may read, but there's nothing to read.
- Pictures: Claude is allowed `Read` in a folder holding only that picture. Codex gets the picture with `-i`.

- `approval_policy="never"` matters: nobody is at a terminal, so the agent must never stop to ask.
- Run, wait (120 s limit), read stdout, done (`AskServer.run`).

### Build: an unattended agent job that writes files

This covers building or updating a plan and making flashcards (`BuildRunner.swift`).

```bash
# Claude Code: cwd = the Simeon folder
claude -p "/build-subject \"<export>\" …" \
  --output-format stream-json --verbose --permission-mode acceptEdits \
  --add-dir <export folder> \
  --allowedTools Read Write Edit Glob Grep Skill Task Agent \
                 "Bash(python3:*)" "Bash(node:*)" "Bash(ls:*)" "Bash(mkdir:*)" "Bash(unzip:*)" … \
  --model <id> --effort <level>

# Codex
codex exec "<procedure + job>" --sandbox workspace-write -c 'approval_policy="never"' --skip-git-repo-check \
  -c model_auto_compact_token_limit=160000 -c tool_output_token_limit=6000 \
  --add-dir <export folder> -o <report.txt>
```

- **Working directory as permission boundary.** The process runs with cwd set to the Simeon folder. `--add-dir` lets it
  *read* the download folder. Claude Code is given an allowlist of tools and shell-command prefixes; Codex gets
  `workspace-write`.
- **Skills vs. inlined procedure.** Claude Code gets the slash command `/build-subject …` and loads
  `.claude/skills/build-subject/SKILL.md` itself. Codex has no skills, so `BuildRunner.prompt` pastes the skill's text
  (front matter removed) into the prompt: "Follow this procedure exactly … Run it now: /build-subject …". **The procedure
  lives in one Markdown file that both agents follow.** That file is the actual product logic.

- **Unattended.** The prompt ends with: *"You are running unattended from the Simeon Mac app: don't ask questions.
  Make sensible assumptions and list them in your final report."* stdin is `/dev/null`.
- **Progress.** The skill tells the agent to print `SIMEON STEP 3/9 — Reading past papers`. The app matches that with a
  regex (`stepRE`) to drive the progress bar. Claude's `stream-json` is parsed line by line: `system/init` (which model),
  `assistant` text and `tool_use` (shown as "Read: Syllabus.md"), and `result` (done or failed). Codex prints plain text,
  so its final report comes from the file passed with `-o`.

- **Resumable.** The agent keeps `PROGRESS.md` in its work folder. If the Mac sleeps, *Continue building* starts a new
  run told to read it first.
- **Checks outside the model.** The agent runs ordinary Python (`course_context check-plan`, the tests) as part of the
  procedure, so the result is checked by code rather than trusted.

## 3. Getting the browser page to the agent: a loopback server

The study page is a local HTML file. A browser page can't start processes, so the Mac app runs a tiny HTTP server
(`AskServer.swift`) on **127.0.0.1:8765**:

- `POST /ask`, `POST /chat` → builds a prompt → `claude -p` / `codex exec` → JSON answer back to the page.
- Locked down three ways: it listens only on 127.0.0.1; it accepts only allowed origins (`null` for `file://`, plus
  the local preview); and every request must carry a **page key** (`X-Simeon` header). The app writes that key into
  the student's own page at build time (`PageKey.swift`). Any website can send the `null` origin, so the origin alone
  proves nothing. The custom header also forces a CORS preflight.

Without these guards, any web page could make the user's agent run prompts on their subscription.

## 4. The other direction: Simeon inside Claude and Codex (MCP)

Simeon also plugs *into* the agents. `simeon-mcp` (`local/mac-app/MCP/main.swift`) is an MCP server over stdin/stdout (no
port). One switch in Settings registers it with `claude mcp add --scope user` / `codex mcp add`, falling back to
editing `~/.claude.json` / `~/.codex/config.toml`. It also copies a skill into `~/.claude/skills/simeon` and
`~/.agents/skills/simeon`, and offers a `.mcpb` for Claude Desktop. The user can then say "quiz me" inside Claude or
ChatGPT, and the agent calls Simeon's tools (`quiz_me`, `reveal`, `save_mark`, …) against local files. Again, the
user's own subscription runs the chat.

So the pattern works both ways: **your app drives the agent** (subprocess) and **the agent drives your app** (MCP).

## 5. On Windows: same design, a few traps

The Windows app (`desktop/`, Tauri: an HTML/JS window over a Rust core) does exactly the same. `assistant.rs` is
`Assistant.swift` in Rust, `jobs.rs` is `BuildRunner`, and `server.rs` is the same loopback server on 127.0.0.1:8765 with
the same page key. It uses the same skills, the same study page and the same Python engine. `cargo test` fails if the
Swift and Rust prompts drift apart. What's different:

| | Mac | Windows |
|---|---|---|
| **Install** | `curl -fsSL https://claude.ai/install.sh \| bash` | `irm https://claude.ai/install.ps1 \| iex` in PowerShell |
| | Codex: `curl … chatgpt.com/codex/install.sh \| sh` | Codex: `npm install -g @openai/codex` |
| **Where the binary lives** | `~/.local/bin`, Homebrew, the Claude app's copy | `%USERPROFILE%\.local\bin\claude.exe`, `%APPDATA%\npm\codex.cmd`, `%LOCALAPPDATA%\Programs\…` |
| **Prompt** | passed as an argument | passed on **stdin** (`claude -p` with no text, `codex exec -`) |
| **Codex sandbox** | macOS's own | `-c windows.sandbox="unelevated"`; if that still blocks everything, the next run uses `danger-full-access` |
| **Shell tools allowed** | `Bash(python3:*)` … | also `PowerShell(...)` rules and the venv's `Scripts\python.exe` |
| **Stopping a job** | `kill <pid>` | `taskkill /PID <pid> /T /F` (the whole process tree) |

The Windows-only traps, each one found in practice:

- **npm installs are `.cmd` shims.** Windows can't start a `.cmd` directly, and running it through `cmd.exe` mangles the
  arguments. `proc::npm_script` reads the shim, finds the `.js` it points to, and runs `node <script>` directly
  (`proc::command_for`).
- **Why the prompt goes on stdin.** Through `cmd`, an argument is cut at the first newline, and a `>` or `&` in it runs
  as a command. A command line is also limited to 32,767 characters, and a build prompt with the procedure pasted in
  is longer than that. stdin avoids all three problems. The Mac could do the same; it just never needed to.

- **No console windows.** Every child process starts with `CREATE_NO_WINDOW` (`proc::quiet`); otherwise a black console
  window opens for each call.
- **Claude Code on Windows needs Git for Windows** (it runs its tools in Git Bash). `fix_for` spots that error and says
  to install Git from git-scm.com.

- **Codex's Windows sandbox.** Unless `windows.sandbox` is set, a `workspace-write` session comes out read-only and
  every command is refused. When the report shows the sandbox blocked everything and nothing was written,
  `codexNoSandbox` is turned on and *Continue building* runs again without it. That is how Claude Code runs on Windows
  anyway.
- **Data folder.** The app's data lives in `%APPDATA%\Simeon`, in the same layout as the Mac's, and Simeon itself is
  unpacked to `%USERPROFILE%\Simeon` by the app (`setup.rs`), since there's no `install.sh` on Windows.

**Lesson for reuse:** build the adapter cross-platform from day one. Pass prompts on stdin, resolve npm shims yourself,
hide console windows, and kill the whole process tree.

## 6. Things we had to deal with

- **Model choice and usage.** Every job defaults to the "everyday" model (newest Sonnet). Claude Code's own default on
  Max is Opus, and one Opus build used up a friend's whole allowance (15.8M tokens against 5–7M on Sonnet). Heavy
  options get a red "Heavy on usage" tag.
- **Codex context growth.** An agent resends its whole conversation every step, so we cap it: auto-compact past 160k
  tokens and at most 6k tokens of any command's output.

- **The CLIs change.** Flags get renamed: `--ask-for-approval` stopped working after `exec`, so it became
  `-c approval_policy="never"`. Keeping them in one file means one place to fix.
- **Training settings.** Consumer plans may train on the data. Before the first job, a pop-up links to each company's
  opt-out setting. Ollama skips it.

- **Fully local option.** `.ollama` is Codex with `--oss --local-provider ollama -m <model>` and its own
  never-signed-in `CODEX_HOME`, so it talks only to the local Ollama and not to chatgpt.com. It needs no subscription
  and sends nothing off the Mac.
- **Doctor screen.** It checks: installed, signed in, internet reaches the company, a real one-line test question
  answered. Each failure has a button that fixes it.

---

## Reusing it in other software: a recipe

1. **Write an adapter**, one module per CLI with: `find()`, `isSignedIn()`, `signIn()` (open Terminal on the official
   login), `install()`, `askArgs(prompt)` and `jobArgs(prompt, workdir, readDirs)`. Nothing else in your app should know
   which agent it is talking to.
2. **Put the logic in a Markdown procedure** (a Claude Code skill). Give it to Claude as `/skill`, and paste it into the
   prompt for Codex. Have it print progress lines you can parse, and keep its state in a file so it can resume.
3. **Use the sandbox, not the prompt, for safety.** Ask mode: no tools and an empty cwd. Job mode: an allowlist of
   tools and command prefixes, writes limited to one folder, `--add-dir` for inputs. Never prompt for approval
   (approval policy "never"). Send the prompt on stdin rather than as an argument: on Windows that is required
   (see section 5).
4. **Read structured output where you can** (`--output-format stream-json`, Codex's `-o` final-message file), and match
   errors as text to turn them into fix buttons.
5. **If a browser UI needs it**, use a loopback server with a per-install secret header. Never accept requests on the
   origin alone.
6. **Expose your app back as an MCP server** so users can drive it from inside their own Claude or ChatGPT.
7. **Protect the user's limits:** default to a cheaper model, warn about heavy settings, and cap context.

Useful for any desktop tool whose AI work is "read these local files, think, write results":
document processing, codebase/report generators, personal knowledge tools, data cleanup, local agents for
email or notes exports. You pay no per-token cost, keep no keys, and run no server, and the user's data goes only to the
AI company they already chose.

**Caveat to check per product:** this relies on each vendor's terms allowing their CLI to be scripted by another app
for the user's own use. It works and both CLIs document headless use (`-p`, `exec`), but read the current terms before
building a business on it, especially if you redistribute or resell the access.

### Where to look in the code

| What | File |
|---|---|
| Flags, finding, sign-in for each agent | `local/mac-app/Sources/Assistant.swift` |
| Install and sign in via Terminal | `local/mac-app/Sources/InstallGuide.swift` |
| Build jobs: prompt, process, stream-json parsing | `local/mac-app/Sources/BuildRunner.swift` |
| Loopback server for the page | `local/mac-app/Sources/AskServer.swift`, `PageKey.swift` |
| MCP server + registering it | `local/mac-app/MCP/main.swift`, `Sources/SimeonInClaude.swift` |
| The procedure itself | `.claude/skills/build-subject/SKILL.md` |
| Windows/Tauri version of the same | `desktop/src-tauri/src/assistant.rs`, `jobs.rs`, `server.rs` |
