← all notes
AI DevEx Meta

Using Codex to set up Codex: when the AI configures itself

I built a bridge to ChatGPT's local App Server so users could generate without API keys. Then I realized Codex could install itself.

Mahmoud
writes his own harnesses
August 9, 2026
13 min read

I shipped something last week that felt like crossing a threshold I didn’t know existed. Shotluma, my AI-powered App Store screenshot editor, now works with your ChatGPT plan instead of requiring API keys. You open the hosted app, click Connect Codex, and paste one prompt into the ChatGPT desktop app. Codex downloads a bridge script, verifies it, starts a local server that talks to its own App Server, pairs it with your browser session, and tells you it’s ready. You return to the browser and generate screenshots through your existing ChatGPT subscription. No git clone, no API key, no bun install, no .env file.

The part that keeps landing weird: the AI sets itself up. You ask Codex to connect Shotluma, and Codex configures Codex.

The thing I didn’t know was there

Shotluma ran entirely in the browser (projects in IndexedDB, no backend, no account system) but AI generation needed to call a model. For months that meant either bringing your own API key or running the dev server locally with keys in .env.local. Both worked, but both had friction.

Then I was debugging something unrelated in Codex and ran ps aux | grep codex to see what processes it spawned. One line caught my attention:

codex app-server --listen stdio://

I’d never seen that before. I ran it manually, and it started an interactive JSON-RPC server on stdin/stdout. I sent it a raw {"method": "initialize", ...} handshake, and it responded with server capabilities, model catalog, account info: everything.

Turns out this is well-documented if you know where to look. Codex includes built-in skills documentation that covers the App Server protocol in detail. It’s just not in the typical places you’d search (official API docs, npm packages, public schemas). But inside Codex itself, the documentation is comprehensive: method signatures, parameters, dynamic tool registration, thread lifecycle, the works.

The thing that made it interesting: it supports dynamic tool registration. You can tell App Server “here are some functions this model can call,” and when the model invokes them during a run, App Server sends you a JSON-RPC request with the tool name and arguments. You execute the tool, send back the result, and the model sees it as native output.

That meant I could connect a browser on app.shotluma.com to this local server, register Shotluma’s canvas tools (add text, place device mockup, adjust layout), and let ChatGPT models call them as if they were built into Codex. No API keys, no separate billing, just routing through the localhost server that was already running.

The opportunity was too good to ignore. The catch was getting it running on user machines without requiring a git clone or a twenty-step setup guide.

How App Server actually works

Codex App Server is the local JSON-RPC service that Codex (ChatGPT’s coding agent) uses internally. When Codex runs a command or reads a file, it’s talking to App Server over stdio. When it calls a GPT model, App Server handles the ChatGPT authentication and routes the request.

The useful part: App Server exposes this over a documented JSON-RPC protocol. You can spawn it as a child process, send it messages, and it responds. The methods I needed:

  • account/read returns the ChatGPT account email and plan type
  • thread/start creates an ephemeral conversation thread with custom instructions and dynamic tools
  • turn/start sends a message to the model and streams responses
  • turn/interrupt cancels an in-progress turn
  • thread/delete cleans up when you’re done

Dynamic tools are the key. When you start a thread, you pass an array of tool specs (name, description, JSON schema). When the model calls one during a turn, App Server sends you an item/tool/call notification with the tool name and arguments. You execute it, send back the result, and App Server forwards it to the model. From the model’s perspective, these look like native Codex tools.

Here’s what spawning App Server and starting a thread looks like:

import { spawn } from 'node:child_process'

const child = spawn('codex', ['app-server', '--listen', 'stdio://'], {
  stdio: ['pipe', 'pipe', 'pipe'],
})

// Initialize the connection
child.stdin.write(
  JSON.stringify({
    id: 'init',
    method: 'initialize',
    params: {
      clientInfo: { name: 'shotluma', version: '1' },
      capabilities: { experimentalApi: true },
    },
  }) + '\n',
)

// Start a thread with dynamic tools
child.stdin.write(
  JSON.stringify({
    id: 'thread-1',
    method: 'thread/start',
    params: {
      ephemeral: true,
      model: 'gpt-5.6-sol',
      baseInstructions: 'You are embedded in Shotluma...',
      dynamicTools: [
        {
          type: 'function',
          name: 'add_text',
          description: 'Add a text element to the canvas',
          inputSchema: {/* JSON Schema */},
        },
      ],
    },
  }) + '\n',
)

// Listen for tool calls
child.stdout.on('data', (data) => {
  const message = JSON.parse(data.toString())
  if (message.method === 'item/tool/call') {
    // Execute the tool, send back result
    const result = executeCanvasTool(
      message.params.tool,
      message.params.arguments,
    )
    child.stdin.write(
      JSON.stringify({
        id: message.id,
        result: {
          success: true,
          contentItems: [{ type: 'inputText', text: JSON.stringify(result) }],
        },
      }) + '\n',
    )
  }
})

The bridge I built has three pieces:

  1. The browser runs on app.shotluma.com, handles the canvas editor, and makes generation requests.

  2. The bridge is a Node script that listens on 127.0.0.1:47447, spawns codex app-server as a child process, and proxies allowed RPC calls. It validates every request against the paired origin and a random bearer token.

  3. Codex App Server authenticates with ChatGPT, routes model calls, and forwards Shotluma’s canvas tools (add text, place device mockup, etc.) as dynamic tool requests back to the bridge.

When a user generates screenshots:

  1. Browser sends a request to the bridge with the bearer token
  2. Bridge spawns App Server and calls thread/start with Shotluma’s tool specs
  3. Bridge sends turn/start with the user’s prompt and screenshot uploads
  4. App Server routes to a GPT model (determined by the user’s ChatGPT plan)
  5. Model decides to call a tool, like add_text
  6. App Server sends item/tool/call to the bridge
  7. Bridge executes the tool against the canvas state and responds
  8. App Server forwards the result to the model
  9. Model continues, calls more tools, eventually finishes
  10. Bridge deletes the thread and reports completion

The browser never talks to App Server directly. The bridge sits in between, validates every request, and restricts the RPC surface to only the methods needed for generation. Security boundaries:

// Only these RPC methods are allowed through the bridge
const ALLOWED_RPC_METHODS = new Set([
  'account/read',
  'account/rateLimits/read',
  'model/list',
  'thread/start',
  'thread/delete',
  'turn/start',
  'turn/interrupt',
])

const restrictRpcMessage = (message: JsonRpcMessage): JsonRpcMessage | null => {
  if (!isAllowedRpcMessage(message)) return null
  const method = message['method']

  // Force security constraints on every thread
  if (method === 'thread/start') {
    return {
      ...message,
      params: {
        ...message.params,
        approvalPolicy: 'never',
        cwd: workspacePath, // empty read-only workspace
        environments: [],
        sandbox: 'read-only',
      },
    }
  }

  // Force sandbox on every turn
  if (method === 'turn/start') {
    return {
      ...message,
      params: {
        ...message.params,
        approvalPolicy: 'never',
        sandboxPolicy: { type: 'readOnly', networkAccess: false },
      },
    }
  }

  return message
}

Additional security layers:

  • Bridge only listens on 127.0.0.1, never 0.0.0.0
  • Every request requires exact origin match + bearer token
  • Bridge never touches ChatGPT tokens (authentication stays inside App Server)
  • Pairing token stored mode 0600, origin normalized and validated

This worked. I could run it locally, pair my browser, and generate screenshots through my ChatGPT subscription. The problem was getting this setup to work on every user’s machine without platform-specific install scripts.

The deployment problem

Getting the bridge running on one machine takes maybe twenty seconds if you know what you’re doing. Getting it running on every user’s machine is where it falls apart.

The bridge needs to:

  • Download from the exact deployment origin
  • Land in a predictable location (~/.local/share/shotluma/)
  • Have execute permissions
  • Receive the pairing token and origin as arguments
  • Start as a detached background process
  • Confirm it’s ready before the browser checks connection

On macOS with Bun installed, that’s:

curl -o ~/.local/share/shotluma/shotluma-codex-bridge.mjs \
  https://app.shotluma.com/codex/shotluma-codex-bridge.mjs
chmod +x ~/.local/share/shotluma/shotluma-codex-bridge.mjs
bun ~/.local/share/shotluma/shotluma-codex-bridge.mjs start \
  --pairing-token <TOKEN> --allowed-origin https://app.shotluma.com

On Linux with Node:

mkdir -p ~/.local/share/shotluma
curl -o ~/.local/share/shotluma/shotluma-codex-bridge.mjs \
  https://app.shotluma.com/codex/shotluma-codex-bridge.mjs
node ~/.local/share/shotluma/shotluma-codex-bridge.mjs start \
  --pairing-token <TOKEN> --allowed-origin https://app.shotluma.com

On Windows with PowerShell and Node:

New-Item -ItemType Directory -Force -Path "$env:LOCALAPPDATA\shotluma"
Invoke-WebRequest -Uri "https://app.shotluma.com/codex/shotluma-codex-bridge.mjs" `
  -OutFile "$env:LOCALAPPDATA\shotluma\shotluma-codex-bridge.mjs"
node "$env:LOCALAPPDATA\shotluma\shotluma-codex-bridge.mjs" start `
  --pairing-token <TOKEN> --allowed-origin https://app.shotluma.com

Notice the problem? There isn’t one script. There are three, and they diverge on directory paths, download tools, runtime detection (Bun or Node? Which version?), and shell quoting. I could put all three in the README and tell users to pick the right one, but “pick the right one” is exactly where people stop.

I could write one polyglot shell script that detects everything, but then I’m maintaining a fragile install script that has to handle Windows PowerShell vs CMD vs WSL, macOS vs Linux directory conventions, permission errors, missing curl, Node vs Bun vs Deno runtime detection, and the combinatorial explosion when two of those assumptions are wrong.

The alternative: ask users to clone the repo, install dependencies, read the setup docs, run a setup script, and debug whatever fails. That’s hazing, not deployment.

What I needed was something that could read the current platform, inspect the script safely, choose the right commands, handle errors, and report what happened. Something with filesystem access and shell execution that could adapt to what it finds on the machine.

I already had that. It was Codex.

The prompt that installs itself

The solution turned out to be simpler than the problem. Instead of writing three platform-specific install scripts, I wrote one prompt that asks Codex to do the install:

Download https://app.shotluma.com/codex/shotluma-codex-bridge.mjs and
inspect it. If it looks safe, save it to ~/.local/share/shotluma/ (or the
Windows equivalent), make it executable if needed, and run:

  <runtime> ~/.local/share/shotluma/shotluma-codex-bridge.mjs start \
    --pairing-token <PAIRING_TOKEN> \
    --allowed-origin https://app.shotluma.com

Use bun if available, otherwise node. Confirm the bridge is running on
localhost before finishing.

(The real prompt is slightly longer and includes the actual pairing token, but that’s the shape.)

When you paste that into Codex:

  1. Codex reads the script from the URL. It sees it’s a standalone Node module, checks for obvious red flags (does it phone home? does it read unrelated files? does it bind to 0.0.0.0?), and reports what it does.

  2. Codex checks the platform. It runs uname or equivalent, detects macOS/Linux/Windows, and picks the right directory path.

  3. Codex checks available runtimes. It looks for bun and node, picks whichever is available (preferring Bun), and confirms the version is recent enough.

  4. Codex downloads and saves the script. It creates the directory if needed, writes the file, and sets execute permissions on Unix-like systems.

  5. Codex starts the bridge. It runs the script with the correct arguments, waits for it to bind to localhost, and checks the /readyz endpoint to confirm it’s running.

  6. Codex tells you it’s done. The output is a short summary: “Bridge is running on localhost:47447. Return to Shotluma and check the connection.”

From the user’s perspective, the setup is: copy the prompt, paste it into Codex, press enter, wait ten seconds, go back to the browser. One action, cross-platform, error-tolerant.

Shotluma Codex connection UI showing the setup prompt and "Open in ChatGPT" button

From my perspective as the developer, I wrote one artifact (the prompt) instead of three (install scripts), and that artifact is adaptive. If a user’s machine has Bun, it uses Bun. If ~/.local/share doesn’t exist, Codex creates it. If the download fails, Codex reports why. If the bridge is already running from a previous session, Codex detects that and reloads the config instead of failing.

The install script is a prompt, and the installer is the AI.

Why this works and when it doesn’t

This only works because of a specific confluence of constraints:

  1. The user already has Codex installed. They’re trying to connect Shotluma to Codex, so by definition they have the desktop app or CLI. I’m not asking them to install an AI agent to run an install script; they already have the agent because that’s the whole point.

  2. The script is small and auditable. The bridge is 637 lines of TypeScript, and Codex reads all of it before running anything. A 50 MB binary or a 10,000-line obfuscated bundle would not pass this bar.

  3. The task is bounded and irreversible mistakes are unlikely. The script downloads one file, writes it to one location, and starts one process. It doesn’t modify system files, doesn’t require root, and doesn’t persist across reboots unless you explicitly configure that. If something goes wrong, the blast radius is small.

  4. Failure is debuggable. If the setup fails, Codex’s output explains what failed and why. If the bridge doesn’t start, the error message tells you whether the problem is the runtime, the network, or the script itself. You’re not staring at a silent failure or a cryptic exit code.

  5. The prompt is versioned with the app. The browser generates the prompt dynamically, so it always includes the current deployment origin and the current bridge download URL. Self-hosted deployments automatically generate prompts that pair with their own origin.

This would not work for:

  • A complex app with multi-step setup and configuration files
  • Anything that modifies system state or requires elevated permissions
  • Tools where the AI doesn’t have the necessary access (e.g., a mobile app, a browser extension)
  • Cases where the user doesn’t already have an AI agent with shell and filesystem access

But for “download this script, put it here, run it with these arguments, tell me when it’s ready” (which describes a surprisingly large category of localhost developer tools) it works better than anything else I’ve tried.

The meta layer

There’s something quietly strange about writing a feature where Codex configures Codex. The prompt I wrote doesn’t run on my machine during development. It runs on thousands of other machines, executed by the same model that helped me write it, setting up infrastructure so that model can later execute more code I wrote (the canvas tool calls).

The user asks Codex to enable Shotluma. Codex downloads and inspects the bridge. The bridge proxies tool definitions to App Server. The user generates a screenshot. App Server routes the request to 5.6 Luna/Terra/Sol. That model calls canvas tools through the bridge. The bridge validates and forwards them. The browser applies them. The result is a screenshot set.

Shotluma connection UI showing the Codex setup flow

The AI is both the installer and the installed. It’s the setup script and the runtime. It reads my code, decides whether to trust it, executes it, and then executes more of my code through the infrastructure it just set up.

I don’t have a grand theory about what this means. But I do notice that this pattern (self-setup prompts) resolves a real tension in developer tools. Localhost tooling is powerful because it has access to the filesystem, the shell, environment variables, and the local network. But that power comes with setup cost: paths differ across platforms, runtimes vary, permissions fail, and “works on my machine” is the most expensive four words in software.

Hosted tools avoid setup cost by running in the cloud, but they lose localhost access. You can’t connect to 127.0.0.1:47447 from a server in us-east-1.

Self-setup prompts give you both. The app is hosted, the infrastructure is localhost, and the setup is delegated to an agent that already has the access the setup needs. You don’t install Codex to set up the bridge. You already installed Codex because you wanted Codex. The bridge setup is just another task you give it.

What I’d do differently

If I were building this again, I’d change a few things:

Make the bridge self-updating. Right now, if I ship a new bridge version, users have to re-run the setup prompt to download the new script. The bridge could check the deployment origin for a newer version on startup and download it automatically, or prompt the user to approve the update.

Add a status endpoint for the browser. The browser checks connection by hitting /v1/status, but that only tells you the bridge is running. It doesn’t tell you which bridge version, or whether App Server is healthy, or how long the session has been active. A richer status response would let the browser show “Connected · ChatGPT Plus · Bridge v2 · Session 3h” instead of just “Connected.”

Handle Codex updates better. If OpenAI ships a breaking change to App Server’s RPC protocol, the bridge will stop working, and the error message will be opaque. The bridge could version-check App Server on startup and refuse to start (with a clear message) if the protocol is incompatible.

Offer a manual install path. Some users won’t trust the AI-driven setup, and that’s fine. The README should include the platform-specific manual install commands as a fallback.

Log setup failures. Right now, if the setup prompt fails, I only know because a user reports it. The bridge could optionally send anonymized failure telemetry (bridge version, platform, error type) to help me fix common issues. This would need to be opt-in and clearly disclosed.

Try it

If you want to see it in action, open app.shotluma.com, click “Generate with AI,” select a Codex model, and click “Connect Codex.” You’ll see the setup prompt. Copy it, paste it into the Claude desktop app or CLI, and watch it configure itself.

The full PR includes the bridge implementation, the browser pairing flow, the connection dialog, and the dynamic tool integration with App Server. The bridge script is standalone and readable; you can review exactly what the setup prompt will run.

And if you build something with a self-setup prompt, I’d like to hear about it. The pattern is young enough that I’m still learning where it works and where it doesn’t.

Sources