Skip to content

About

HTTP wrapper around OpenAI Codex CLI image_gen — generate and edit images from anywhere, no local codex install

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

codexcloud

An HTTP wrapper around the OpenAI Codex CLI image_gen tool, running in a Linux container. You POST a prompt (optionally with input images), it runs codex exec inside a scratch job directory, and returns the generated PNG as the response body.

Point of the thing: image generation from anywhere — a cloud agent session, a container, CI, a phone — without a local codex install or a local login. One box holds the Codex auth; everything else just curls it.

POST /codeximg    { prompt }                    -> image/png
POST /codeximgin  { prompt, images[{name,b64}] } -> image/png   (edit / reference images)
GET  /health      -> codex login status
GET  /            -> "ok" (no auth)

This repo contains no credentials. You bring your own Codex login and your own service token. See .env.example.


Setup — instructions for Claude

You are setting this service up for the user. Work through the phases in order; do not skip the verification at the end of each. Anything that needs the user's own account (Codex login, Railway login) is theirs to do — ask for it once, in one message, rather than discovering each blocker separately.

Phase 0 — what you need from the user

Before touching anything, confirm all three. Ask in a single message:

  1. A machine with Codex CLI already logged in. ~/.codex/auth.json must exist on the machine you are running from. Check it yourself:
    ls -l ~/.codex/auth.json && codex login status
    If it's missing, the user runs npm i -g @openai/codex && codex login (a browser flow — you cannot do it for them). A ChatGPT-account login is what this is built for; an OPENAI_API_KEY login also works but bills per image.
  2. A host that supports a persistent volume. Instructions below use Railway; any Docker host works, the only hard requirement is that CODEX_HOME survives restarts.
  3. Confirmation that a shared image-generation endpoint is acceptable to them — anyone holding the bearer token generates images on the user's Codex account.

Phase 1 — deploy the container

Railway (what this was built and tested on):

npm i -g @railway/cli
railway login          # browser flow — the user's, not yours
railway init           # create/select the project, from THIS directory
railway up --detach    # builds the Dockerfile, deploys

Then, in the Railway dashboard for the service (or via the CLI):

  • Add a volume mounted at /data. Without it the codex auth is re-seeded from env on every boot and refreshed tokens are lost — the service will start dying after the first token rotation. This is the single most common setup mistake.
  • Generate a public domain for the service; note the URL.

Any other Docker host: docker build -t codexcloud . && docker run -p 8080:8080 -v codexdata:/data --env-file .env codexcloud.

Phase 2 — seed the variables

seed-vars.sh does both in one shot, from a railway-linked directory on the Codex-logged-in machine. It mints a SERVICE_TOKEN, base64s the local auth.json, and never prints the auth payload:

./seed-vars.sh          # writes the token to .service-token (gitignored)
railway redeploy -y

Doing it manually instead:

railway variables \
  --set "SERVICE_TOKEN=$(openssl rand -hex 24)" \
  --set "CODEX_AUTH_B64=$(base64 < ~/.codex/auth.json | tr -d '\n')"

How the auth actually flows: CODEX_AUTH_B64 is a bootstrap copy only. entrypoint.sh writes it to $CODEX_HOME/auth.json on first boot (volume empty) or when FORCE_RESEED=1; after that codex refreshes tokens in place on the volume. Never treat the env var as the live credential.

Phase 3 — verify before you report success

URL=https://<your-service>.up.railway.app
TOKEN=$(cat .service-token)

curl -s "$URL/"                                              # -> ok
curl -s -H "Authorization: Bearer $TOKEN" "$URL/health"      # -> "Logged in using ChatGPT"

If /health doesn't say logged in, stop and fix it — every generation will fail.

Then a real end-to-end generation. Takes 1–3 minutes; --max-time 600 is mandatory or curl gives up before codex finishes:

curl -s -X POST "$URL/codeximg" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  --max-time 600 -o out.png \
  -d '{"prompt": "Use the image_gen tool to generate an image of a red bicycle against a white wall. Then view the generated image to confirm it depicts that."}'
file out.png     # must say PNG image data — if it says JSON/ASCII, the job failed

Read out.png yourself and confirm it shows what was asked. A 200 with a PNG body proves the pipeline ran, not that the image is right; never report success off the exit code alone, and never trust codex's own "verified" claim in the transcript.

Phase 4 — hand over

Give the user: the service URL, where the token lives (.service-token, gitignored), and the two curl recipes below. Tell them the token is the only thing gating access to their Codex account, so it goes in a secret store or a gitignored env file — never in a committed file, a chat message they'd forward, or a public repo.


Calling it

Save the endpoint and token somewhere gitignored, e.g. .env:

. ./.env       # sets URL and TOKEN

Text to image

curl -s -X POST "$URL/codeximg" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  --max-time 600 -o out.png \
  -d '{"prompt": "Use the image_gen tool to generate an image of <SUBJECT>. Then view the generated image to confirm it depicts <SUBJECT> and describe what you see."}'

Write the prompt exactly like that: state the subject, then ask codex to view and confirm — it self-corrects on the second look. Do not append a "save to ./out.png" instruction; the server appends that contract itself.

Editing / referencing input images

Images ride in as base64 in the JSON body. Build the body with python — base64 payloads blow past shell argument limits:

python3 - <<'EOF'
import base64, json, pathlib
def b64(p): return base64.b64encode(pathlib.Path(p).read_bytes()).decode()
body = {
  "prompt": (
    "You are given an input image (attached, also at ./input.png): <describe it>. "
    "Use the image_gen tool WITH this attached image as the input image to EDIT it: <the edit>. "
    "Keep EVERYTHING else identical: <list what must survive — text verbatim, logo, layout, colors>. "
    "Then view the generated image and confirm the edit happened and the rest is intact."
  ),
  "images": [{"name": "input.png", "b64": b64("source.png")}],
}
pathlib.Path("/tmp/body.json").write_text(json.dumps(body))
EOF

curl -s -X POST "$URL/codeximgin" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  --max-time 600 --data-binary @/tmp/body.json -o out.png

Rules that make the edit path actually work:

  • Say explicitly "WITH this attached image as the input image to EDIT it" — otherwise codex generates from the text alone and ignores the attachment.
  • Name every element that must be preserved. Anything you don't list is fair game.
  • Images attach in array order (-i per file); refer to them as "image 1", "image 2".
  • name values must be unique per request — the server writes them all into one dir and duplicates overwrite each other.
  • Multiple images is the path for character consistency, style transfer, and brand grounding (attach a logo and a type specimen alongside the source).

Failure modes

Symptom Meaning Fix
Response is JSON, not PNG codex ran but produced no out.png; body carries error, exitCode, codexOutputTail Read the tail — usually a prompt or content-policy issue. Rewrite, retry
401 unauthorized Missing/wrong bearer token Check SERVICE_TOKEN matches what you're sending
/health doesn't say "Logged in" Cloud auth expired or was never seeded Run ./push-auth.sh from a machine with a live local codex login
Auth dies every few days No volume at /data, so refreshed tokens vanish on redeploy Mount the volume, then ./push-auth.sh
curl exits 28 --max-time too low Use 600
body too large Input images exceed the 80MB base64 cap Downscale the inputs

Ops

Task Command
Deploy code changes railway up --detach
Logs railway logs
Re-seed codex auth ./push-auth.sh (sets FORCE_RESEED=1, redeploys, clears it)
Rotate the service token railway variables --set "SERVICE_TOKEN=$(openssl rand -hex 24)", then update clients

Security notes

  • The bearer token is the whole perimeter. Whoever holds it generates images on your Codex account, at your cost. Treat it like an API key; rotate if it leaks.
  • Inside the container codex runs with -s danger-full-access, because Landlock isn't available in unprivileged containers — the container is the sandbox. That is a deliberate choice for this disposable, single-purpose box and is not a licence to use that flag on a local machine.
  • Each request runs in its own /tmp/jobs/<uuid> directory, deleted a few seconds after the response. Input images and outputs are not persisted.
  • The Codex version is pinned in the Dockerfile. Bump it deliberately — image_gen behaviour changes between releases.

About

HTTP wrapper around OpenAI Codex CLI image_gen — generate and edit images from anywhere, no local codex install

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages