Skip to content

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-write scoped to that worktree, approval_policy = never (headless can't prompt), minimal per-repo AGENTS.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, exports CODEX_HOME=~/.codex)
  • What: Install the latest Codex CLI to ~/.local/bin. Create ~/.codex/config.toml with approval_policy = "never", sandbox_mode = "workspace-write", [sandbox_workspace_write] network_access = true. Justin runs codex login interactively (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> via git worktree add off a fresh branch codex/<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 for forge, 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 minimal AGENTS.md into 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 (add CODEX_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_AUD pattern) OR bearer CODEX_CHAT_TOKEN. GET /healthz (no auth). Init data/codex_chat.db schema: 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 / from sites/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 in threads. GET /api/threads (list), GET /api/threads/{id} (with messages). POST /api/threads/{id}/images (base64 data-URLs, up to 6, saved to data/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?} streams text/event-stream. Async-spawn codex exec [resume <codex_session_id>] --json --cd <worktree> --sandbox workspace-write --skip-git-repo-check -o <tmp> with prompt on stdin and --images for attachments; CODEX_HOME from ~/.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 to threads.codex_session_id. Persist user + assistant messages. Fully async subprocess, no threads (feedback_no_threads_with_subprocess_run). Non-zero exit → SSE error event, 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 in done, 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 via EventSource/fetch-stream on the turn endpoint. Served Cache-Control: no-cache with versioned assets (launcher lesson). Auth: same-origin behind CF Access in prod; bearer field for direct LAN.
  • Verification: /screenshot https://codex.justinsforge.com after Task 9 (desktop + mobile render of the chat surface); until then load http://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 ingress codex.justinsforge.com -> http://127.0.0.1:7366 on the VR Alliance tunnel fb20b500-0119-4c84-9536-fa85751987c3 (Console-local, required for 127.0.0.1 origin). Add CF_ACCESS_AUD for the new app to forge/.env (comma-append).
  • Verification: curl -fsS -o /dev/null -w '%{http_code}\n' https://codex.justinsforge.com/healthz returns 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, with URL: 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