Plan: codex.justinsforge.com — self-hosted claude.ai/code-style chat surface driving OpenAI Codex on Console¶
URL: https://mkdocs.justinsforge.com/memory/plans/codex-chat-console-surface-2026-07-08/
Date: 2026-07-08
Approved spec: A new FastAPI user service on Console (forge_codex_chat_api.py, port 7366) with its own web chat UI at sites/justinsforge.com/codex/, behind Cloudflare Access + the VR Alliance (Console-local) tunnel at codex.justinsforge.com. It replicates the claude.ai/code experience for OpenAI Codex: thread list + conversation view, pick a repo at spawn, send text and images, watch streaming markdown responses. Each turn shells codex exec [resume <thread>] --json --cd <worktree> --sandbox workspace-write and streams JSONL to the browser over SSE. Threads persist in data/codex_chat.db. Each session runs in its own git worktree (commit-early, like CT dev slots) so Codex never dirties main and cannot trigger forge automations at runtime. Codex auth = ChatGPT sign-in into a dedicated CODEX_HOME, fully isolated from Anthropic creds. Deliberately separate service from the Claude session-launcher.
Out of scope (v1): OpenAI cloud/Codex-web, PR automation, multi-user, mobile Chrome extension, parallel turns within one thread, non-Console targets.
Decisions locked¶
- Auth: ChatGPT sign-in (
codex login), flat plan quota. Justin performs the interactive login in Task 1. - Isolation: per-session git worktree, sandbox
workspace-writescoped to that worktree,approval_policy = never(headless can't prompt), minimal per-repoAGENTS.md. - Repo allowlist v1: chat.justin, CreatorTrack, forge, plus a "new app" scaffold slot.
- Transport: SSE for streaming. Bearer
CODEX_CHAT_TOKEN(forge/.env) as LAN/curl fallback; CF Access JWT is the prod gate.
Task list¶
Task 1: Install Codex CLI + ChatGPT auth in a dedicated CODEX_HOME¶
- Files:
~/.local/bin/codex(binary),~/.codex/config.toml(new),~/.forge-secrets/codex.env(new, chmod 600, exportsCODEX_HOME=~/.codex) - What: Install the latest Codex CLI to
~/.local/bin. Create~/.codex/config.tomlwithapproval_policy = "never",sandbox_mode = "workspace-write",[sandbox_workspace_write] network_access = true. Justin runscodex logininteractively (ChatGPT sign-in) to write~/.codex/auth.json. - Verification:
codex --version && codex exec --help | grep -E 'json|resume|images|sandbox|--cd' && test -f ~/.codex/auth.json && echo AUTH_OK - Commit:
chore(codex): install Codex CLI + dedicated CODEX_HOME on Console
Task 2: Repo registry + worktree helper¶
- Files:
/home/justinwieb/forge/scripts/forge_codex_repos.py(new) - What: Define the repo allowlist (key → source repo path, default branch). Functions:
list_repos(),ensure_worktree(repo_key, session_id) -> path(creates~/codex-work/<repo_key>/<session_id>viagit worktree addoff a fresh branchcodex/<session_id>),teardown_worktree(repo_key, session_id). Fail loud on unknown repo key or dirty add. - Verification:
python3 /home/justinwieb/forge/scripts/forge_codex_repos.py --self-test(creates a throwaway worktree forforge, prints its path, tears it down, exits 0) - Commit:
feat(codex): repo allowlist + per-session worktree manager
Task 3: Minimal coding-only AGENTS.md template¶
- Files:
/home/justinwieb/forge/scripts/forge_codex_agents_md.py(new),/home/justinwieb/forge/templates/codex_AGENTS.md.tmpl(new) - What:
write_agents_md(worktree_path, repo_key)renders a minimalAGENTS.mdinto the worktree root: repo name, "coding only, no forge automations/hooks", commit-early guidance. No forge doctrine, no memory, no Telegram. - Verification:
python3 -c "import sys; sys.path.insert(0,'/home/justinwieb/forge/scripts'); import forge_codex_agents_md as m; p=m.write_agents_md('/tmp','forge'); print(open(p).read()[:200])" - Commit:
feat(codex): minimal coding-only AGENTS.md template writer
Task 4: API skeleton + sqlite schema + CF Access auth¶
- Files:
/home/justinwieb/forge/scripts/forge_codex_chat_api.py(new),/home/justinwieb/forge/.env(addCODEX_CHAT_TOKEN) - What: FastAPI app on port 7366, bind 0.0.0.0.
require_auth()accepts CF Access JWT (reuse launcher's PyJWT-vs-CF_ACCESS_TEAM_DOMAIN/CF_ACCESS_AUDpattern) OR bearerCODEX_CHAT_TOKEN.GET /healthz(no auth). Initdata/codex_chat.dbschema:threads(id, repo_key, title, codex_session_id, worktree_path, created_at),messages(id, thread_id, role, content, images_json, created_at). Mount static UI at/fromsites/justinsforge.com/codex/. - Verification:
python3 /home/justinwieb/forge/scripts/forge_codex_chat_api.py & sleep 2; curl -fsS http://127.0.0.1:7366/healthz | jq .; kill %1 - Commit:
feat(codex): chat API skeleton, sqlite schema, CF Access + bearer auth
Task 5: Thread CRUD + image upload endpoints¶
- Files:
/home/justinwieb/forge/scripts/forge_codex_chat_api.py(edit) - What:
POST /api/threads {repo_key, title?}→ creates worktree (Task 2) + AGENTS.md (Task 3), row inthreads.GET /api/threads(list),GET /api/threads/{id}(with messages).POST /api/threads/{id}/images(base64 data-URLs, up to 6, saved todata/codex_uploads/<thread>/).DELETE /api/threads/{id}tears down worktree. - Verification:
TOKEN=$(grep CODEX_CHAT_TOKEN /home/justinwieb/forge/.env | cut -d= -f2); curl -fsS -H "Authorization: Bearer $TOKEN" -X POST http://127.0.0.1:7366/api/threads -d '{"repo_key":"forge"}' | jq .id - Commit:
feat(codex): thread CRUD + image upload endpoints
Task 6: Codex turn runner + SSE streaming¶
- Files:
/home/justinwieb/forge/scripts/forge_codex_chat_api.py(edit) - What:
POST /api/threads/{id}/turn {prompt, image_ids?}streamstext/event-stream. Async-spawncodex exec [resume <codex_session_id>] --json --cd <worktree> --sandbox workspace-write --skip-git-repo-check -o <tmp>with prompt on stdin and--imagesfor attachments;CODEX_HOMEfrom~/.forge-secrets/codex.env. Parse JSONL events line-by-line, forward as SSE (agent_message,tool,error,done). On first turn capture the codex session id and persist tothreads.codex_session_id. Persist user + assistant messages. Fully async subprocess, no threads (feedback_no_threads_with_subprocess_run). Non-zero exit → SSEerrorevent, loud. - Verification:
TOKEN=...; TID=<from Task 5>; curl -N -H "Authorization: Bearer $TOKEN" -X POST http://127.0.0.1:7366/api/threads/$TID/turn -d '{"prompt":"list the files in the repo root, then stop"}'(observe streamed SSE events ending indone, and a real file listing) - Commit:
feat(codex): codex exec turn runner with JSONL→SSE streaming + thread resume
Task 7: Web chat frontend¶
- Files:
/home/justinwieb/forge/sites/justinsforge.com/codex/index.html(new),.../codex/app.js(new),.../codex/app.css(new) - What: Left rail = thread list + "New thread" (repo picker dropdown from
GET /api/repos). Main = conversation view rendering markdown, a composer with textarea + image attach (drag/paste, up to 6), streaming assistant bubbles viaEventSource/fetch-stream on the turn endpoint. ServedCache-Control: no-cachewith versioned assets (launcher lesson). Auth: same-origin behind CF Access in prod; bearer field for direct LAN. - Verification:
/screenshot https://codex.justinsforge.comafter Task 9 (desktop + mobile render of the chat surface); until then loadhttp://127.0.0.1:7366/locally and confirm the thread list + composer render. - Commit:
feat(codex): web chat frontend (threads, repo picker, image attach, SSE render)
Task 8: systemd user service¶
- Files:
~/.config/systemd/user/forge-codex-chat.service(new) - What: User unit running the working-tree
forge_codex_chat_api.py(edits go live on restart),EnvironmentFile=~/.forge-secrets/codex.env+ forge/.env, linger already enabled.WantedBy=default.target. - Verification:
systemctl --user daemon-reload && systemctl --user enable --now forge-codex-chat && systemctl --user status forge-codex-chat --no-pager | grep -q running && curl -fsS http://127.0.0.1:7366/healthz | jq . - Commit:
feat(codex): forge-codex-chat systemd user service
Task 9: Cloudflare Access app + tunnel ingress¶
- Files: (no repo files; CF state via scripts)
scripts/forge_cloudflare_access_add.py,scripts/forge_cloudflare_cf.py - What: Create CF Access app for
codex.justinsforge.com(email allowlist[email protected]) BEFORE exposing DNS/ingress (launcher lesson: never up unauthenticated). Add ingresscodex.justinsforge.com -> http://127.0.0.1:7366on the VR Alliance tunnelfb20b500-0119-4c84-9536-fa85751987c3(Console-local, required for 127.0.0.1 origin). AddCF_ACCESS_AUDfor the new app to forge/.env (comma-append). - Verification:
curl -fsS -o /dev/null -w '%{http_code}\n' https://codex.justinsforge.com/healthzreturns 200 (through tunnel), and an unauthenticated browser hit shows the CF Access login (not the app). - Commit:
feat(codex): codex.justinsforge.com CF Access app + VR Alliance ingress
Task 10: Register and document¶
- Files:
/home/justinwieb/.claude/projects/-home-justinwieb-forge/memory/MEMORY.md(index line),/home/justinwieb/forge/memory/general/reference_codex_chat_surface.md(new, withURL:line) - What: Write the topic file (service, port, tunnel, auth model, worktree isolation, endpoints, ops commands). Add the MEMORY.md index entry under Tools and Surfaces. Run the eval harness. Per feedback_register_tools.md.
- Verification:
bash /home/justinwieb/forge/scripts/forge_eval_run.sh - Commit:
docs(codex): register codex.justinsforge.com surface in memory