Bring your own subscription
How Simeon runs on the student's own Claude Code or ChatGPT Codex, on the Mac and on Windows, and how to reuse the idea in other software. · 27 October 2026
The whole entry as Markdown, to give to Claude Code, Codex or Cursor: paste it in, or put the file in your project.
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.pathaskszsh -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, andtooOld(...)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.
# 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
Readin 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).
# 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-dirlets it read the download folder. Claude Code is given an allowlist of tools and shell-command prefixes; Codex getsworkspace-write. -
Skills vs. inlined procedure. Claude Code gets the slash command
/build-subject …and loads.claude/skills/build-subject/SKILL.mditself. Codex has no skills, soBuildRunner.promptpastes 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'sstream-jsonis parsed line by line:system/init(which model),assistanttext andtool_use(shown as "Read: Syllabus.md"), andresult(done or failed). Codex prints plain text, so its final report comes from the file passed with-o. -
Resumable. The agent keeps
PROGRESS.mdin 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 (
nullforfile://, plus the local preview); and every request must carry a page key (X-Simeonheader). The app writes that key into the student's own page at build time (PageKey.swift). Any website can send thenullorigin, 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
.cmdshims. Windows can't start a.cmddirectly, and running it throughcmd.exemangles the arguments.proc::npm_scriptreads the shim, finds the.jsit points to, and runsnode <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_forspots that error and says to install Git from git-scm.com. -
Codex's Windows sandbox. Unless
windows.sandboxis set, aworkspace-writesession comes out read-only and every command is refused. When the report shows the sandbox blocked everything and nothing was written,codexNoSandboxis 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%\Simeonby the app (setup.rs), since there's noinstall.shon 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-approvalstopped working afterexec, 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.
.ollamais Codex with--oss --local-provider ollama -m <model>and its own never-signed-inCODEX_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
- Write an adapter, one module per CLI with:
find(),isSignedIn(),signIn()(open Terminal on the official login),install(),askArgs(prompt)andjobArgs(prompt, workdir, readDirs). Nothing else in your app should know which agent it is talking to. - 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. - 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-dirfor 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). - Read structured output where you can (
--output-format stream-json, Codex's-ofinal-message file), and match errors as text to turn them into fix buttons. - If a browser UI needs it, use a loopback server with a per-install secret header. Never accept requests on the origin alone.
- Expose your app back as an MCP server so users can drive it from inside their own Claude or ChatGPT.
- 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 |