Ya escribí la perorata de «por qué le arranqué la fontanería a mis propios repos y construí una imagen base» en el post de aicodebox, así que no la repito aquí. Versión corta: construí claudebox para Claude Code, le hice crecer una API HTTP, un endpoint compatible con OpenAI, un servidor MCP, un bot de Telegram y un scheduler de cron, luego quise exactamente lo mismo para un segundo agente y me negué a copiar y pegar novecientas líneas de FastAPI y de renderizado de markdown para Telegram en un repo nuevo por tercera vez. Así que la fontanería se arrancó y se metió en aicodebox, una imagen base agnóstica al agente, y cada agente concreto pasó a ser una imagen hija fina que solo implementa una clase de adaptador.
codexbox es ese adaptador apuntado al CLI de Codex de OpenAI. La misma base, la misma superficie REST, el mismo bot de Telegram, el mismo scheduler de cron, lo único realmente nuevo aquí es el código que traduce una petición genérica de «ejecuta este prompt» a la combinación demencial de flags que Codex quiera ese día, más el baile de autenticación alrededor de dos formas completamente distintas de pagar por ello.
El Problema Concreto Con el CLI de OpenAI
El CLI de Codex está bien. La ergonomía del CLI de Codex como algo a lo que tiras programáticamente es otra historia. Unos cuantos puntos de leerse la cosa de verdad en vez de fiarse de su documentación:
- Peta al arrancar por un directorio que no existe. Pon
CODEX_HOMEen una ruta y no la crees antes, y codex simplemente da error en vez de hacer elmkdir -pde una línea que literalmente cualquier otro CLI de la Tierra hace por ti. Más sobre esto abajo, porque es el mejor y el peor detalle de todo el repo. - El flag para saltarse el sandbox se lee como una cláusula legal.
--dangerously-bypass-approvals-and-sandbox. No--yolo, no-y, una frase entera, presumiblemente para que nadie pueda alegar que no sabía lo que hacía cuando su container se puso a hacer rm -rf de algo. - Reanudar es un subcomando, no un flag. Cualquier otro CLI de agente que he cableado acepta
--continueo--resume <id>como flag en el comando normal de ejecución. Codex te obliga a llamar aexec resume <id>oexec resume --lastcomo verbo distinto, lo que significa que el adaptador tiene que construir una forma de argv completamente distinta según si estás reanudando o no. - No hay un interruptor de «apaga todas las herramientas».
update_planes incondicional yapply_patchse queda mientras exista un entorno local, así que un modo sin herramientas de verdad significa quitar las herramientas de shell y de búsqueda web por config y forzar el sandbox a solo lectura como medida doble, porque la config sola no lo puede capar del todo. - La salida estructurada solo acepta una ruta de fichero. Codex tiene validación nativa de esquema JSON vía
--output-schema, lo cual es sinceramente mejor que lo que tienen los otros adaptadores de esta base, no hacen falta reintentos de autocorrección, pero solo acepta un fichero en disco, no un esquema inline, así que el adaptador tiene que escribir tu esquema en un fichero temporal en cada llamada. - Su propio flujo JSON te miente sobre ser JSON. Ejecuta
codex exec --jsony te sale un flujo JSONL deThreadEventpor stdout, solo que codex también intercala líneas de log en texto plano del tipoERROR ...directamente en ese mismo stdout. No puedes dar por hecho que cada línea parsea. Parseas, capturas el fallo de decodificación, lo cuentas, sigues.
Nada de esto hace malo a Codex. Hace de Codex un CLI construido para un humano tecleando en una terminal, no para un programa tirando de él en bucle, que es exactamente el hueco que el adaptador existe para tapar.
Cuatro Variables de Entorno y Un mkdir
El Dockerfile pone exactamente cuatro variables de entorno para cablear el adaptador en la imagen base, todas en un solo bloque:
ENV AICODEBOX_ADAPTER=codexbox.adapter:CodexAdapter
AICODEBOX_AGENT_BINARY=codexbox-agent
CODEXBOX_IMAGE_VARIANT=minimal
CODEX_HOME=/home/aicode/.codexAICODEBOX_ADAPTER apunta la maquinaria de carga de adaptadores de la base a CodexAdapter para que sepa construir argv específicamente para codex. AICODEBOX_AGENT_BINARY mete un script de lanzamiento en lugar de llamar a codex directamente para el uso interactivo y de passthrough (por qué, abajo). CODEXBOX_IMAGE_VARIANT solo señala en qué imagen estás (minimal aquí, full en la build con toolchain). Y CODEX_HOME es la que de verdad importa, por ese crash de arranque que mencioné.
Codex lee auth.json, config.toml y sus ficheros de desarrollo de sesión desde $CODEX_HOME. Si esa variable no está puesta, codex cae a un valor por defecto sensato. Pero si sí está puesta, que aquí tiene que estarlo, porque todo el sentido es bind-montearla desde el host para que un login sobreviva a un container reventado y recreado, y el directorio al que apunta todavía no existe, codex se niega a arrancar. Ni un aviso, ni una creación automática, un error duro. Así que justo después del bloque ENV, el Dockerfile hace esto:
RUN mkdir -p /home/aicode/.codex && chown -R aicode:aicode /home/aicode/.codexUn solo mkdir -p y un chown, horneados en la imagen en tiempo de build, puramente para que un CLI escrito por una empresa con cien mil millones de dólares en el banco no se caiga la primera vez que lo apuntas a un bind mount recién hecho. Este es sinceramente mi detalle favorito de todo el repo, no porque sea listo, es lo contrario de listo, es un apaño para una llamada a mkdir que falta. Pero es el tipo de cosa que solo encuentras leyendo de verdad el Dockerfile en vez de fiarte de un README, y explica por qué CODEX_HOME se pre-crea en lugar de simplemente declararse y dejar que codex se apañe.
Lo Que CodexAdapter Implementa De Verdad
El contrato de adaptador de la imagen base te da un puñado de métodos que rellenar, y CodexAdapter los implementa todos: validate (rechaza valores desconocidos de esfuerzo de razonamiento, avisa e ignora una allowlist de herramientas para la que codex no tiene equivalente), build_argv (la carne, traduce una petición genérica de ejecución a la sopa real de flags de codex), translate_auth (no hace nada, porque codex lee OPENAI_API_KEY / OPENAI_BASE_URL / auth.json de forma nativa, sin aliasado), parse_output y parse_events (decodifican el flujo JSONL en un resultado normalizado, saltándose las líneas de log no-JSON intercaladas), parse_stream_event (convierte líneas individuales en deltas canónicos de flujo para los endpoints de streaming en vivo), interactive_argv y passthrough_argv (invocación cruda del binario codex para TUI y passthrough), y auth_paths (le dice a la imagen base dónde vive el fichero de credenciales para que pueda comprobar su existencia).
build_argv es donde viven las decisiones interesantes. Una ejecución sin resume ni noContinue puestos cae por defecto en exec resume --last, continuando por defecto la sesión más reciente del workspace, la misma idea que claudebox. systemPrompt se mapea a -c instructions=..., que sustituye el prompt de sistema integrado de codex; appendSystemPrompt se mapea a -c developer_instructions=..., que en cambio añade un mensaje con rol de developer al lado. El esfuerzo de razonamiento va por -c model_reasoning_effort=<level> y no por un flag de pensamiento dedicado. Y cada uno de estos se pasa como un único elemento de argv, no interpolado por shell, así que el texto de prompt multilínea y los prompts de sistema sobreviven literalmente sin que los destroce un shell en algún punto de la tubería.
Lo Que codexbox-agent.sh Restaura De Lo Que La Base Deja Caer
El modo passthrough de aicodebox es deliberadamente tonto: para invocaciones interactivas y de un solo tiro simplemente ejecuta exec $AICODEBOX_AGENT_BINARY "$@" y se aparta. Eso vale para una imagen base genérica, pero significa que los valores por defecto del lado del container, sin peticiones de aprobación, continuación de sesión sensata, no se aplican automáticamente al propio TUI interactivo de Codex, solo a los modos de servidor dirigidos por el adaptador. codexbox-agent.sh es el script enchufado vía AICODEBOX_AGENT_BINARY para devolver esos valores por defecto específicamente a las rutas de TUI y de passthrough de CLI:
case "${1:-}" in
login | logout | mcp | mcp-server | doctor | completion | update | resume | review | apply | sandbox | debug | features | help | -V | --version | -h | --help)
exec "$CODEX_BIN" "$@"
;;
exec | e)
# inject --dangerously-bypass-approvals-and-sandbox unless already present
exec "$CODEX_BIN" "$sub" "$BYPASS" "$@"
;;
esac
# bare interactive TUI — defaults to resuming the workspace's last session
exec "$CODEX_BIN" resume --last "$BYPASS" ${args[@]+"${args[@]}"}Los subcomandos de auth y de mantenimiento (login, logout, mcp, doctor, update, etcétera) se ejecutan completamente tal cual, sin flags inyectados, y eso es lo que permite que codexbox login --device-auth dirija el flujo OAuth de ChatGPT sin tocarlo. Cualquier otra cosa, una sesión interactiva pelada o una llamada a exec, recibe el flag de bypass inyectado automáticamente para que no tengas que teclear --dangerously-bypass-approvals-and-sandbox a mano cada vez, y el TUI pelado recibe además el mismo valor por defecto de «continúa la última sesión de este directorio» que usa el adaptador en modo servidor. Los modos de servidor (API, Telegram, cron, MCP) no tocan este script en absoluto, van por CodexAdapter.build_argv y levantan codex directamente.
Auth Doble: Clave de API o Suscripción de ChatGPT, Un Solo Fichero
Codex soporta dos modos de autenticación y codexbox tiene que mantener los dos funcionando sin pisarse. Un script de init que corre una vez por arranque (10-codex-auth-config.sh) lee cualquier auth.json existente, comprueba su campo auth_mode, y solo siembra una clave de API vía codex login --with-api-key cuando no hay auth existente o la existente ya está en modo apikey. Un login OAuth de suscripción de ChatGPT gana siempre y nunca se sobreescribe, aunque OPENAI_API_KEY resulte estar puesta en el entorno. La razón de que la clave de API necesite un paso de login siquiera en vez de leerse directamente de la variable de entorno: el subcomando exec de codex no acepta una OPENAI_API_KEY pelada para autenticarse, necesita auth.json escrito en disco primero.
Ambos modos de auth convergen en exactamente el mismo fichero, que es también exactamente por lo que CODEX_HOME tiene que sobrevivir a que revienten el container. CodexAdapter.auth_paths() devuelve una sola ruta:
def auth_paths(self) -> list[str]:
home = os.environ.get("HOME", "/home/aicode")
cfg = os.environ.get("CODEX_HOME", f"{home}/.codex")
return [f"{cfg}/auth.json"]Bind-montea ~/.codex desde el host, y sea como sea que te autenticaste, con una clave de API guardada o con un token OAuth de ChatGPT, sobrevive a cada recreación del container, porque no vive en la capa escribible del container, vive en el montaje que el directorio pre-creado y con chown de la base hizo seguro para apuntar CODEX_HOME ahí en primer lugar.
Mínima frente a Full: La Misma Auth, El Mismo Adaptador, Más Toolchain
La imagen por defecto psyb0t/codexbox:latest es codex más Node, Python, uv, Docker, git, jq y curl, lo justo para ejecutar el agente y poco más. Dockerfile.full construye una segunda imagen encima de la mínima, fijada por digest a un tag publicado concreto, y apila el mismo toolchain de desarrollo de propósito general que trae la imagen full de claudebox: Go con golangci-lint, un toolchain de Python vía pyenv, un toolchain de Node instalado desde un lockfile de pnpm comiteado con los scripts de ciclo de vida desactivados, GitHub CLI, Terraform, kubectl, Helm, y el montón habitual de herramientas de build, clientes de base de datos y depuradores, repartidos en los directorios de dependencias full-go/, full-node/ y full-python/ para que las entradas de cada toolchain estén fijadas por versión y verificadas por suma de comprobación en vez de «lo que a apt le apetezca instalar hoy». El mismo adaptador de Codex, el mismo entrypoint, el mismo comportamiento de auth, la build full solo añade herramientas alrededor, y cada descarga en ella se verifica por suma de comprobación contra un SHA256 fijado antes de darla por buena.
El Lado del Host: wrapper.sh e install.sh
install.sh baja la imagen elegida (mínima por defecto, full vía CODEXBOX_FULL=1), crea ~/.codex y un directorio de claves SSH para git sobre SSH dentro del container, descarga wrapper.sh, hornea en él el tag de imagen resuelto, y lo instala en tu PATH como codexbox. A partir de ahí:
export OPENAI_API_KEY=sk-... # or: codexbox login --device-auth
codexbox # interactive TUI, continues last session for this dir
codexbox --no-continue # same, but forces a fresh session
codexbox exec "fix the failing test" # one-shot exec, output to your terminal
echo "summarize README.md" | codexbox exec -
codexbox stop # kill this dir's running container(s)
codexbox clear-session # drop saved codex sessions, keep auth + configEl trabajo del wrapper es deliberadamente estrecho: resolver la imagen, montar el workspace, el ~/.codex persistido y el socket de docker, reenviar la auth y cualquier variable CODEXBOX_ENV_* / CODEXBOX_MOUNT_*, y gestionar un container por directorio, por nombre. Cada decisión real de flag de codex, inyección del bypass, reanudar frente a sesión nueva, passthrough de subcomandos, vive dentro de la imagen en codexbox-agent.sh, no en el script del host. El wrapper pasa tus argumentos intactos y deja que el container decida qué hacer con ellos.
Instalar Lo Que Acabas de Construir, No Lo Que Hay en Docker Hub
La ruta de instalación daba por hecho que querías la imagen publicada. Vale para usarla, inútil para trabajar en ella, porque construirías una imagen local, ejecutarías el instalador, y este se iría a bajar la copia del registry por encima de tus cambios.
Así que ahora hay targets para eso:
make install # build the minimal image, then install the wrapper against it
make install-full # same, full variant
make install-wrapper # wrapper only, against an image you already builtLos tres pasan por CODEXBOX_SRC_LOCAL=true, que le dice a install.sh que se salte el docker pull y use la imagen local elegida. Falla a gritos si esa imagen no se ha construido, en lugar de caer en silencio a bajarla, porque todo el sentido es no pillar la publicada. Ese flag también funciona por su cuenta si llamas a install.sh directamente.
make install-wrapper es al que de verdad vas a recurrir una y otra vez: los arreglos del wrapper del host ya no exigen reconstruir la imagen solo para probar un cambio en un script de shell.
Los Comandos de Gestión Dejaron de Okupar Tu Sesión
El wrapper lo pasaba todo por el container interactivo persistente. Correcto para una sesión de código de verdad y equivocado para los subcomandos de mantenimiento, plugin, doctor, sandbox, debug, review, apply, resume, archive, delete, que ahora reciben cada uno un container --rm desechable.
Relacionado: cada subcomando de Codex de primer nivel soportado se reenvía ahora tal cual, en vez de tratarse como una petición interactiva de reanudación. Teclear un subcomando de verdad te daba antes una sesión que no habías pedido.
Puedes quedarte con lo que Codex dijo de verdad
Por defecto la respuesta es el resumen útil. Pon eventMode a "full" en la ejecución y te salen en cambio los registros propios de Codex intactos: razonamiento, ejecución de comandos, cambios de ficheros, actividad MCP, actividad web, actualizaciones de todo, consumo. Nada plegado en un turno, nada truncado para dejar la carga útil aseada. Esto importa sobre todo cuando una ejecución hizo algo que no esperabas y el resumen es justo la capa que tiró las pruebas.
Tres arreglos por los que merece la pena pasar del pin
codex update fallaba con EACCES. codex estaba instalado en el prefijo global por defecto de npm bajo /usr, lo que deja el paquete en manos de root en /usr/lib/node_modules, mientras que el container corre como el usuario sin privilegios aicode. Así que el propio autoactualizador del CLI no podía escribir en su propia instalación. Ahora se instala bajo /home/aicode/.local, con un /home/aicode/.npmrc que fija el mismo prefijo a través de la bajada de privilegios del entrypoint.
La imagen full heredaba un fósil. Dockerfile.full apuntaba por defecto a una base que había derivado de vuelta a v0.2.0, así que latest-full se construía sobre una imagen con varios meses de retraso respecto a latest. El flujo escalonado publica ahora primero la imagen mínima y la variante full hereda la actual, que es lo que «la misma base, más toolchain» debía significar desde el principio. Toda esa clase de bug desapareció a partir de la v0.5.11, porque codexbox dejó de tener un toolchain propio del que derivar: las herramientas compartidas de Go, Node, Python, editores, clientes de base de datos y ops se mudaron arriba, a psyb0t/aicodebox:v0.15.0-full, y la imagen full de codexbox ahora simplemente parte de ella. Borrando de paso unas 15.800 líneas de lockfiles. Ya no queda una segunda copia que se pueda quedar atrás.
Los subagentes robaban la conversación. La continuación de Codex se reanuda por workspace, y un despliegue de subagente más reciente podía secuestrar el hilo que estaban continuando el cron, la API, MCP o Telegram. Ahora persiste el hilo de exec de primer nivel confirmado por workspace canónico y reanuda exactamente ese ID, así que una ejecución de subagente no puede convertirse calladamente en aquello de lo que tu trabajo de cron continúe mañana.
Cajas que lanzan cajas
Instala codexbox, claudebox y pibox en el mismo directorio y cada wrapper monta las otras dos en solo lectura en /usr/local/bin/<name>. Codex puede entonces llamar a otro agente a mitad de sesión, lo cual sirve sobre todo cuando quieres un segundo modelo mirando el mismo diff.
Las ejecuciones anidadas llevan un contexto de host versionado, porque «dónde está el workspace» tiene una respuesta distinta dentro de un container que en la máquina que lo arrancó: AICODEBOX_LAUNCH_CONTEXT_VERSION=1, más AICODEBOX_HOST_HOME, AICODEBOX_HOST_WORKSPACE, AICODEBOX_HOST_WRAPPER_DIR y un home y una ruta de wrapper por agente, para cada uno de los tres. Una ejecución anidada se salta además las escrituras de ficheros de auth que hace una de primer nivel, así que un hijo no puede reescribir las credenciales de quien lo llamó.
AICODEBOX_ENV_* y AICODEBOX_MOUNT_* reenvían entorno y montajes a todas las cajas de golpe, junto a los CODEXBOX_ENV_* y CODEXBOX_MOUNT_* de cada caja. AICODEBOX_MANAGED_INSTALL=1 les da a los scripts de aprovisionamiento una instalación no interactiva que no sobreescribirá una clave SSH existente, con CODEXBOX_INSTALL_DIR y CODEXBOX_BIN_NAME decidiendo dónde aterriza y cómo se llama. El instalador además baja wrapper.sh del tag de release correspondiente en vez de master, así que una instalación fijada se lleva el wrapper de esa versión.
Todas Las Entradas a la Caja
Más allá de la shell interactiva y del exec de un solo tiro, las mismas superficies de servidor de aicodebox se aplican aquí, solo que ejecutando codex por debajo:
- Modo API: un servidor FastAPI con
/run(ejecuciones de agente síncronas o asíncronas),/files/*(listar, leer, escribir y borrar dentro del workspace, con comprobación de traversía de rutas), y un endpoint/openai/v1/chat/completionscompatible con OpenAI. Exige queCODEXBOX_AVAILABLE_MODELSesté puesta explícitamente, porque codex no tiene una lista de modelos hardcodeada, la dirige el servidor, así que no hay un valor por defecto sensato al que caer. - Modo Telegram: entra texto, se ejecuta codex, y el Markdown se renderiza de vuelta a HTML. Sobreescrituras por chat para modelo, esfuerzo de razonamiento y prompts de sistema, persistidas entre reinicios.
- Modo Cron: trabajos programados dirigidos por croniter que disparan codex con una instrucción fija según un horario, con cada ejecución registrada en un directorio de historial por trabajo.
- Modo MCP: la superficie MCP propia de la imagen base, con sus operaciones de ficheros y su ejecución de prompts, montada en
/mcpcuando el modo API está activo o como proceso sidecar si no. Esto es aparte de las capacidades propias de cliente y servidor MCP de codex, que no están cableadas en nada de esto.
Lo único que merece decirse explícitamente: toolsAllowlist y noTools se aceptan en /run por compatibilidad de API con los otros agentes de esta base, pero codex no tiene una allowlist de herramientas integradas con nombre. Una allowlist se registra y se ignora. noTools es la única excepción que sí se honra, quita las herramientas de shell y de búsqueda web de la petición y fuerza el sandbox a solo lectura, porque la config sola no puede quitar apply_patch ni update_plan.
Mereció la Pena
codexbox no es la reescritura de nada. Es lo que «añade otro agente» se supone que debe costar una vez que el trabajo real de infraestructura está hecho en otra parte: un Dockerfile, cuatro variables de entorno, un mkdir -p para un directorio que el propio CLI de OpenAI no se crea solo, y una clase de Python que conoce el vocabulario concreto de flags de Codex. Todo lo demás, la API, el bot de Telegram, el scheduler de cron, la superficie MCP, el wrapper de host entero, es de aicodebox, intacto.
Si quieres toda la argumentación de la imagen base, léete el post de aicodebox. Si quieres el hermano de Claude Code, ese es claudebox. Si solo quieres una caja que ejecute Codex sin que el CLI de OpenAI se dé de bruces con un directorio que falta, está en GitHub. Hace un solo trabajo y dejó de darse de bruces, que es todo lo que siempre quise de ella.
Cómo Instalarlo En Tu Agente
Codex puede instalar la cosa que ejecuta Codex. Todo lo que hay bajo .agents/ está catalogado en un solo marketplace, así que son dos comandos:
claude plugin marketplace add psyb0t/agents
claude plugin install codexbox@psyb0tCodex usa el mismo marketplace con un verbo distinto, codex plugin add codexbox@psyb0t, porque no existe codex plugin install. También encuentra el skill por su cuenta en un checkout del repo, ya que escanea .agents/skills/ de forma nativa sin nada instalado en absoluto. Además ahora está listado en el MCP Registry oficial, así que un cliente que resuelva servidores desde ahí puede encontrarlo sin que le den una URL.