Run a v2-only Node JSONL proxy that spawns `codex app-server` and exposes an automation-friendly stream API. Use when you need to drive the app-server programmatically (automation/orchestration/session mining), have subagents send updates or code patches into live sessions, auto-handle approvals, forward server requests with deterministic timeouts, mine sessions via `thread/*`, steer active turns (`turn/steer`), or run N parallel instances (each instance is one proxy + one app-server child).
Resources
3Install
npx skillscat add tkersey/dotfiles/cas Install via the SkillsCat registry.
cas (App-Server Control)
Overview
Cas ships a small Node proxy (scripts/cas_proxy.mjs) that:
- Spawns
codex app-server. - Performs the required handshake (
initialize->initialized) withexperimentalApi: trueand optionaloptOutNotificationMethods. - Reads/writes JSONL over stdio.
- Auto-accepts v2 approval requests.
- Forwards v2 server requests to the orchestrator.
- Rejects deprecated legacy approval requests (
execCommandApproval,applyPatchApproval). - Fails forwarded requests deterministically on timeout (default
30000ms). - Emits a lossless, automation-friendly event stream (includes the raw app-server message plus derived routing keys).
This skill assumes codex is available on PATH and does not require access to any repo source tree.
Quick Start
CODEX_SKILLS_HOME="${CODEX_HOME:-$HOME/.codex}"
CLAUDE_SKILLS_HOME="${CLAUDE_HOME:-$HOME/.claude}"
CAS_SCRIPTS_DIR="$CODEX_SKILLS_HOME/skills/cas/scripts"
[ -d "$CAS_SCRIPTS_DIR" ] || CAS_SCRIPTS_DIR="$CLAUDE_SKILLS_HOME/skills/cas/scripts"
run_cas_tool() {
local subcommand="${1:-}"
if [ -z "$subcommand" ]; then
echo "usage: run_cas_tool <smoke-check|smoke_check|instance-runner|instance_runner> [args...]" >&2
return 2
fi
shift || true
local cas_subcommand=""
local marker=""
local fallback=""
case "$subcommand" in
smoke-check|smoke_check)
cas_subcommand="smoke_check"
marker="cas_smoke_check.zig"
fallback="$CAS_SCRIPTS_DIR/cas_smoke_check.mjs"
;;
instance-runner|instance_runner)
cas_subcommand="instance_runner"
marker="cas_instance_runner.zig"
fallback="$CAS_SCRIPTS_DIR/cas_instance_runner.mjs"
;;
*)
echo "unknown cas subcommand: $subcommand" >&2
return 2
;;
esac
if command -v cas >/dev/null 2>&1 && cas --help 2>&1 | grep -q "cas.zig"; then
if cas "$cas_subcommand" --help 2>&1 | grep -q "$marker"; then
cas "$cas_subcommand" "$@"
return
fi
echo "cas binary found, but marker check failed for subcommand: $cas_subcommand" >&2
return 1
fi
if [ "$(uname -s)" = "Darwin" ] && command -v brew >/dev/null 2>&1; then
if ! brew install tkersey/tap/cas; then
echo "brew install tkersey/tap/cas failed; refusing silent fallback." >&2
return 1
fi
if command -v cas >/dev/null 2>&1 && cas --help 2>&1 | grep -q "cas.zig"; then
if cas "$cas_subcommand" --help 2>&1 | grep -q "$marker"; then
cas "$cas_subcommand" "$@"
return
fi
echo "brew install tkersey/tap/cas did not produce a compatible cas $cas_subcommand subcommand." >&2
return 1
fi
echo "brew install tkersey/tap/cas did not produce a compatible cas binary." >&2
return 1
fi
if [ -f "$fallback" ]; then
node "$fallback" "$@"
return
fi
echo "cas binary missing and fallback script not found: $fallback" >&2
return 1
}
run_cas_tool smoke-check --cwd /path/to/workspace --jsonTerminology (Instances)
- An "instance" is one
cas_proxyprocess plus its spawned app-server child process. - Each instance has its own JSONL stream and its own
sessionId. - "N instances" means N parallel proxy+app-server pairs; it is not N threads/turns inside one instance.
- Isolation tip: for multi-instance runs, prefer per-instance
--state-file(or the runner's--state-file-dir) if you don't want instances to share state.
Trigger cues
- "instances" / "multi-instance" / "parallel sessions"
- app-server control (JSONL proxy, JSON-RPC methods)
- session mining (thread/turn inventory, export/index)
- steering/resume (
turn/steer,thread/resume)
Workflow
Start the proxy.
- Run
node scripts/cas_proxy.mjsfrom the cas skill directory (or resolve by script path:CODEX_SKILLS_HOME="${CODEX_HOME:-$HOME/.codex}"; CLAUDE_SKILLS_HOME="${CLAUDE_HOME:-$HOME/.claude}"; CAS_PROXY="$CODEX_SKILLS_HOME/skills/cas/scripts/cas_proxy.mjs"; [ -f "$CAS_PROXY" ] || CAS_PROXY="$CLAUDE_SKILLS_HOME/skills/cas/scripts/cas_proxy.mjs"; node "$CAS_PROXY"). - Optional: pass
--cwd /path/to/workspaceto control where the app-server runs. By default, state is written under~/.codex/cas/state/<workspace-hash>.json. - Optional: pass
--state-file PATHto override the default state location. - Optional: tune forwarded request fail-fast behavior with
--server-request-timeout-ms <N>(0 disables timeout). - Optional: control v2 approval auto-responses (useful for safe multi-instance workers):
--exec-approval auto|accept|acceptForSession|decline|cancel--file-approval auto|accept|acceptForSession|decline|cancel--skill-approval auto|approve|decline--read-only(shorthand for declining exec + file + skill approvals)
- Optional: pass one or more
--opt-out-notification-method METHODflags to suppress known noisy notifications for the connection. - Wait for a
cas/readyevent.
For N instances in parallel, prefer the instance runner:
run_cas_tool instance-runner --cwd /path/to/workspace --instances N
- Run
Drive the app-server by sending requests to the proxy.
- Send
cas/requestmessages (method + params) to proxy stdin. - Proxy assigns request ids (unless you supply one), forwards to app-server, and emits
cas/fromServerresponses. - Optional smoke check: run
run_cas_tool smoke-check --cwd /path/to/workspace.
- Send
Stream and route notifications.
- Consume
cas/fromServerevents and route bythreadId/turnId/itemId. - Treat the proxy stream as the source of truth; the raw wire message is always included under
msg.
- Consume
Handle forwarded server requests.
- Only reply when cas emits
cas/serverRequest(these are the server requests cas did not auto-handle). - Respond with
cas/respondusing the sameid. - If your response is malformed for a typed v2 request, cas sends a deterministic JSON-RPC error upstream instead of hanging.
- If you do not reply in time, cas emits
cas/serverRequestTimeoutand fails that request upstream. - Approvals are auto-accepted by default (including best-effort execpolicy amendments and skill approvals) and will not block you unless you override approval policy flags.
- Only reply when cas emits
Mine sessions (optional).
- Use
thread/list(cursor pagination + optionalmodelProviders/sourceKinds/archived/cwdfilters) andthread/read(optionallyincludeTurns:true) to build your own index. - The server is not a search engine; extract data and index externally.
- Use
Dedicated API Helpers
Use scripts/cas_client.mjs convenience wrappers when you want typed intent rather than raw method strings:
resumeThread(params)->thread/resumesteerTurn(params)->turn/steerlistExperimentalFeatures(params)->experimentalFeature/list
Dynamic Tools (Optional)
If you opt into dynamic tools, register them on thread/start via dynamicTools (experimental API surface).
When the server emits cas/serverRequest:
- For
method: "item/tool/call", run the tool in your orchestrator and reply withcas/respond. - For
method: "item/tool/requestUserInput"(experimental), collect answers and return{ answers: ... }. - For
method: "skill/requestApproval"(experimental), return{ decision: "approve" }or{ decision: "decline" }. - For
method: "account/chatgptAuthTokens/refresh", return refreshed tokens or a deterministic error.
Proxy I/O Contract (stdin/stdout)
The proxy itself speaks JSONL over stdio.
stdin -> cas
cas/requestsends a JSON-RPC request tocodex app-server:
{
"type": "cas/request",
"clientRequestId": "any-string",
"method": "thread/start",
"params": { "cwd": "/path", "experimentalRawEvents": false }
}cas/respondanswers a server-initiated request forwarded by cas:
{
"type": "cas/respond",
"id": 123,
"result": {
"contentItems": [{ "type": "inputText", "text": "..." }],
"success": true
}
}cas/sendforwards a raw JSON-RPC message tocodex app-server(advanced escape hatch):
{
"type": "cas/send",
"msg": { "method": "thread/list", "id": "raw-1", "params": { "cursor": null } }
}cas/state/getemits the current proxy state.cas/stats/getemits a stats snapshot (uptime, queue depth, counts).cas/exitshuts down the proxy.
stdout <- cas
cas/readyindicates the proxy finished handshake.cas/fromServeris emitted for every JSON message fromcodex app-server.cas/toServeris emitted for every JSON message sent tocodex app-server(includes auto-approvals and handshake).cas/serverRequestis emitted for server-initiated requests that require an orchestrator response (tool calls, auth refresh, etc.).cas/serverRequestTimeoutis emitted when a forwarded server request is failed due to timeout.cas/statsandcas/ioPaused/cas/ioResumedhelp you monitor backpressure.
All events include:
seq(monotonic)ts(ms since epoch)sessionId(unique per proxy instance)- derived keys
threadId/turnId/itemIdwhen present msg(the raw app-server message; lossless)
Canonical Schema Source
Use your installed codex binary to generate schemas that match your version:
codex app-server generate-ts --out DIR
codex app-server generate-json-schema --out DIR
# If you need experimental methods/fields (e.g. dynamic tools), include:
codex app-server generate-ts --experimental --out DIR
codex app-server generate-json-schema --experimental --out DIRLocal References
Read references/codex_app_server_contract.md for a control map and the recommended routing/response strategy.
Resources
references/
Control notes for fast lookup during implementation.
scripts/
Runnable Node proxy for orchestration.
Included:
scripts/cas_proxy.mjs(the proxy)scripts/cas_client.mjs(JS wrapper: spawn proxy + request() + event stream)scripts/budget_governor.mjs(helpers: rateLimits -> per-window pacing + stricter-tier clamp)scripts/cas_rate_limits.mjs(CLI: prints normalizedaccount/rateLimits/readsnapshot)scripts/cas_example_orchestrator.mjs(example orchestration script)scripts/cas_instance_runner.mjs(run one method across many parallel cas sessions/instances)scripts/cas_smoke_check.mjs(smoke-checksexperimentalFeature/list,thread/resume,turn/steer)
Runtime bootstrap policy for Zig CLIs mirrors seq: prefer installed cas dispatcher binary (cas smoke_check / cas instance_runner); on macOS with brew, treat brew install tkersey/tap/cas failure (or incompatible binary/subcommand marker) as a hard error; otherwise fallback to the local Node script.