A Codex Responses router that gives agents verified atomic edits and direct script execution, without Code Mode wrapper ceremony.
hpatch-router sits between Codex and the Responses API. The model sees
constrained functions.hpatch and free-form functions.shell. Successful
calls still return native Codex carriers, so sandbox checks, permissions,
command sessions, and the normal diff UI stay intact. The repository also
exposes the reusable Go edit engine used by the router.
| Goal | Start here |
|---|---|
| Install and route Codex through hpatch | Codex router |
| See what you get | Features |
| Understand verified editing | Why hpatch? |
| Run programs without Code Mode wrappers | Why shell? |
| Inspect live usage | Metrics |
| Use the engine without Codex | Go library |
| Read contracts | doc/spec/index.md |
- Codex still owns sandboxing, permissions, process sessions, and the visible patch diff. The router translates; it does not silently commit workspace edits.
- A human-readable dashboard lives at the router root, for example
http://127.0.0.1:8080/. The same listener serves Responses, models, and/api/metrics. - A systemd user unit is the intended long-running setup. Passthrough mode forwards Responses traffic without installing hpatch or plugins.
- Codex keeps seeing stream activity while a tool call is validated; the router withholds untranslated input until the complete payload is ready.
- Optional
--capture-outputappends sanitized JSONL for later inspection. Outcome hooks can record each routed hpatch or recovery result.
functions.hpatchis a verified atomic edit language: copy aLINE:HASHrow, emit the new text once, and let the router build the patch.functions.shellsends the program body in its native syntax. Compact shebangs select the interpreter; Bash is the default.- Private shell commands stay inside Bash and POSIX programs:
hreadfor verified rows,hgrepfor text search,hsymbolfor language-server lookup, andinspect_filefor a structural outline with no source bodies. - Eligible programs can be retained, inspected, edited, and rerun through
@shell/references. Wholly stale-target rejections usefunctions.hpatch_recoverinstead of rewriting the whole script. - A Lark grammar constrains HPATCH syntax as the model writes it. Supported languages are validated before Codex applies the patch. Configured plugins can join the model-visible catalog.
HPATCH_DIAGNOSE=1adds a free-formreport_issuetool for agent-experience problems. SeeREQ-DIAGNOSE-001.- Subagent activity shows the requested role, model, reasoning effort, exact spawn and follow-up messages, and exact replies as commentary. These messages are visible in Codex but are removed before later model requests.
- The hpatch family avoids repeating old context.
hpatchidentifies a verified region and writes the replacement once.hread,hgrep, andhsymbolemit copyableLINE:HASHrows.inspect_filereturns structure instead of file bodies. functions.shelldrops the JavaScript carrier, JSON argument object, and extra quoting layers that Code Modeexec_commandrequires.- Opt-in CTP/2 is a lossless encoding of eligible model-visible request strings and assistant text between the Hpatch-projected request and the provider. Repeats inside one string can become a local dictionary; tool outputs may instead point at earlier visible output lines in the same request. Newly emitted tool names and payloads stay native. Validated compaction requests stay native and skip CTP/2.
- A Lark grammar constrains HPATCH generation so the model does not have to retry invalid syntax. A completed invalid script is still rejected atomically.
- Opt-in
--mentor-handofftemporarily sends eligible spawnedgpt-5.6-lunaandgpt-5.6-terrachildren asgpt-5.6-solwith high reasoning, then returns to the Codex-configured model. Only an AgentControlcollab_spawnwithsubagent_kind: thread_spawnactivates it; ordinary sessions and forks stay unchanged. SeeREQ-MENTOR-001.
A direct Code Mode edit makes the model repeat patch framing, old context, replacement text, and a JavaScript carrier. The router moves patch reconstruction out of model output:
flowchart LR
subgraph output["Alternative model-output payloads"]
H["hpatch path<br/>functions.hpatch + verified targets + replacement"]
A["apply_patch baseline<br/>functions.exec + JavaScript carrier<br/>+ old context + replacement + patch framing"]
end
subgraph router["Router and Codex after model output"]
B["Router reads the immutable<br/>workspace baseline"]
C["Router generates the<br/>apply_patch envelope"]
D["Codex applies the patch<br/>sandbox checks + normal diff"]
end
H --> B --> C --> D
A --> D
The patch is not eliminated: the router generates it after inference.
For an 11-line function replacement, hpatch asks the model for this:
functions.hpatch
in parser.go
type 42:e217..52:d10b <<PATCH
func parse(input []byte) (Document, error) {
tokens, err := tokenize(input)
if err != nil {
return Document{}, fmt.Errorf("tokenize: %w", err)
}
document, err := buildDocument(tokens)
if err != nil {
return Document{}, fmt.Errorf("build document: %w", err)
}
return document, nil
}
PATCH
Direct apply_patch in Code Mode repeats all 11 old lines, then writes the
same 11 new lines plus patch framing and the JavaScript carrier. Hpatch writes
the new function once and identifies the old region with two verified rows.
That smaller payload is only one benefit. Editing becomes a verified transaction: targets check an immutable invocation baseline, a bad command rejects the whole script, and supported language validation runs before Codex applies anything. Grammar is syntax only; missing files, stale rows, and conflicting edits still fail atomically.
See REQ-SCRIPT-001, REQ-SELECT-001,
and REQ-OUTPUT-001. Authoritative agent workflow:
contrib/codex/file-editing-instructions.md.
Native tools.exec_command is Codex's execution backend. Calling it from Code
Mode makes the model generate a JavaScript program, a JSON argument object, a
quoted command, and an output projection. functions.shell is an adapter to
that same executor: the model sends the program body directly.
#!python3
print("hello")| Concern | Code Mode tools.exec_command |
functions.shell |
|---|---|---|
| Model output | JavaScript wrapper, argument object, quoted command, and output projection | Exact script body |
| Quoting | Program text can cross JavaScript, JSON, and shell quoting layers | No outer heredoc or command-string wrapper |
| Interpreter | Encoded in the command construction | Compact shebang; Bash is the default |
| Standard input | Arranged through the wrapper | Remains available to the program |
| Correction | The model must emit the program again | Eligible programs can be retained, inspected, edited, and rerun |
| Execution policy | Codex native executor | The same Codex native executor, sandbox, permissions, and result |
This is better for the harness because it removes syntax that exists only to reach the executor. It is not a claim that the underlying process runs faster.
A one-line Bash program with no shebang or directive and containing one
external command is sent directly to the native executor, so a call such as
rtk shadowtree test . remains that command. Composed scripts, private
commands, and other interpreters use the generated
shell <interpreter> <program> carrier.
shell can start PTY-backed, interactive, and long-running programs and
forwards the native executor's complete result. If execution yields a session
handle, use Codex's native session facilities; each shell call starts a new
execution. See OpenAI's Codex prompting guide
and REQ-SHELL-001.
- Go 1.26 or newer. Normal
go installdoes not require a checkout. - Hpatch router mode requires Codex CLI with ChatGPT auth from
codex loginso each request carries a Bearer token and ChatGPT account header. Codex normally stores that file auth at~/.codex/auth.jsonor$CODEX_HOME/auth.json. At startup, the router reads the adjacentconfig.tomlonly to determine whethermodel_instructions_fileis configured. - Hpatch router mode resolves Node.js 24 or newer as
node; passthrough mode does not load the plugin registry. - Private hgrep requires
rgon the Codex executor'sPATH. - Private hsymbol requires the resolver for the queried language on the Codex
executor's
PATH:goplsfor Go, TypeScript 7 astscfor JavaScript, TypeScript, and JSON, andpyright-langserverfor Python.pyand.pyisources. - Private hread, hgrep, hsymbol, and inspect_file are evaluated inside Bash or
POSIX shell programs. The separately installed, fixed
shellhelper must be on the Codex executor's trustedPATH. - The built-in shell uses the embedded
mvdan/sh, including bash and sh shebangs; other selected interpreters must be available through the inheritedPATHor a direct path. - Source builds that regenerate the embedded plugin with
make installorgo generaterequire Bun.make installadditionally requires Make.
make install regenerates the embedded built-in plugin bundle and installs
hpatch-router plus the fixed shell helper through go install:
make installInstallation and uninstallation never create, edit, or remove Codex
configuration or instruction files. make uninstall removes only the
installed hpatch-router and shell binaries.
The mandatory builtin.shell implementation comes from plugins/shell.mjs
and is embedded during generation; make install does not copy it into user
configuration.
Configured plugins are direct regular .js or .mjs files in
$XDG_CONFIG_HOME/hpatch/plugins or ~/.config/hpatch/plugins on Linux. The
router loads them in lexical order into one immutable process snapshot. It
does not discover workspace-local or remote plugins and does not hot-reload
files. Restart hpatch-router after any plugin change. Invalid modules,
duplicate identities, or an unusable built-in registry fail startup before the
router listens. The module contract is REQ-PLUGIN-001.
Everyday functions.shell use is a free-form program. Compact shebang,
interpreter selection, and session behavior are in Why shell?
and REQ-SHELL-001.
A retained result includes retained: true and a script_ref such as
@shell/<call-id>. The artifact is scoped to the Codex thread, expires after
one hour by default, and is not a workspace file. Its thread directory is
removed when the router shuts down.
hread @shell/<call-id>Copy an emitted LINE:HASH row into a complete hpatch script whose paths are
all under @shell/. Do not mix retained and workspace paths in one script.
Rerun the current retained body with:
#!script=@shell/<call-id>
Retained edits use router-owned storage rather than the workspace
apply_patch carrier. See REQ-SHELL-001.
Hread, hgrep, hsymbol, and inspect_file are recognized only by the Bash and POSIX shell evaluators:
hread parser.go 20:40
hgrep -e 'TranslateForHostAt' .
hsymbol refs internal/router/server.go 42:abcd Run 2
inspect_file internal/router/server.go | jq -c '.data.outline[]'Use hread before editing; inspect_file lines are not HPATCH targets. Complete
inputs and failure behavior: REQ-READ-001,
REQ-GREP-001, REQ-SYMBOL-001,
and REQ-INSPECT-001.
In hpatch mode, the router validates authentication and turn metadata,
constructs the plugin registry, and installs standalone functions.hpatch and
functions.shell. Configured contributions marked model-visible join that
catalog. Hread, hgrep, hsymbol, and inspect_file remain authenticated
shell-internal commands.
Defaults:
| Setting | Default |
|---|---|
| Mode | hpatch (--mode); passthrough forwards Responses traffic without loading the tool registry |
| Model protocol | native (--model-protocol); ctp2 is opt-in and Hpatch-only |
| Mentor Handoff | Disabled (--mentor-handoff); Hpatch-only |
| Provider base URL | https://chatgpt.com/backend-api/codex (--provider-base-url) |
| Listen | 127.0.0.1:8080 (--listen) |
| Upstream response-start timeout | 10m (--timeout) |
| Upstream stream idle timeout | 4m of inactivity between bytes (--stream-idle-timeout) |
| Auth | Codex-managed ChatGPT credentials, typically ~/.codex/auth.json or $CODEX_HOME/auth.json; Codex owns login and refresh |
| Shell runtime directory | $HPATCH_RUNTIME_DIR, or the operating-system temporary directory when unset; router and executor must resolve the same absolute path |
| Capture output | Disabled; --capture-output PATH appends sanitized JSONL |
| Hooks | $XDG_CONFIG_HOME/hpatch or ~/.config/hpatch |
| Endpoints | GET / dashboard, POST /v1/responses, GET /v1/models, and GET /api/metrics, all on one listener |
--provider-base-url changes where the router sends Codex-managed credentials
and Responses traffic. Use it only with a trusted endpoint.
Use --mode passthrough to forward Responses traffic without installing
hpatch, shell, private commands, or rejected-script recovery. Capture remains
available because it observes the transport.
Use --model-protocol ctp2 for the compact provider representation. See
REQ-CTP-001. Use --mentor-handoff only for the
spawned-subagent schedule above. See REQ-MENTOR-001.
In hpatch mode, run the router as the same login user as Codex so it can open the absolute workspace paths Codex sends. Codex attaches its managed credentials to each request. A user systemd unit is the intended long-running setup.
go install github.com/yusing/hpatch/cmd/hpatch-router@latest \
github.com/yusing/hpatch/cmd/shell@latestThe binaries are installed under $GOBIN, or under $(go env GOPATH)/bin
when GOBIN is unset. Ensure that directory is on the router and Codex
executor PATH.
Install the published user-unit template:
mkdir -p ~/.config/systemd/user
curl -fsSL https://raw.githubusercontent.com/yusing/hpatch/main/contrib/systemd/hpatch-router.service \
-o ~/.config/systemd/user/hpatch-router.service
systemctl --user daemon-reload
systemctl --user enable --now hpatch-router.service
systemctl --user status hpatch-router.serviceOptional: keep the service after logout:
loginctl enable-linger "$USER"One-shot without the unit (still uses the installed binary):
hpatch-router --listen 127.0.0.1:8080If auth lives outside ~/.codex, or the binary is not in ~/go/bin, use a
drop-in:
systemctl --user edit hpatch-router.service[Service]
Environment=CODEX_HOME=%h/.codex
ExecStart=
ExecStart=%h/.local/bin/hpatch-router --listen 127.0.0.1:9090Then systemctl --user daemon-reload && systemctl --user restart hpatch-router.service.
Add a Responses provider in ~/.codex/config.toml:
[model_providers.hpatch]
name = "hpatch"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = trueMake it the default for the whole config:
model_provider = "hpatch"Or select it for one invocation with a profile. Put
model_provider = "hpatch" in ~/.codex/hpatch.config.toml (the provider
block can live in the base config or in that file), then:
codex --profile hpatchYou can also overlay the same setting without a profile file:
codex -c 'model_provider="hpatch"'Hpatch mode requires valid turn metadata, but its wire workspaces member is
optional. No usable directory does not block the turn, never falls back to
router cwd, and permits only absolute hpatch operands.
Useful checks:
systemctl --user status hpatch-router.service
journalctl --user -u hpatch-router.service -f
curl -sS http://127.0.0.1:8080/api/metrics
curl -sS http://127.0.0.1:8080/v1/modelscontrib/codex/file-editing-instructions.md
is the single source for CTP/2 representation rules and all durable HPATCH,
shell, hread, hgrep, hsymbol, and inspect_file workflow guidance. The router
applies it in memory and never reads or writes the configured instruction file.
The carrier is a nonempty top-level instructions string, or the first
textual developer message when that field is missing, null, or empty. A
recognized stock Codex file-editing section is replaced. A customized prompt
without that section receives the hpatch section only when
model_instructions_file is set in Codex's config.toml; without that
setting, a missing section fails before forwarding. Validated compaction
requests skip this rewrite. The router snapshots the setting at startup;
restart it after adding or removing the key. See
REQ-GUIDE-001.
The module path is github.com/yusing/hpatch. The root package exposes
workspace evaluation, application, reporting, and host translation APIs.
Root-scoped application APIs use a caller-authorized *os.Root and
root-relative cwd. Host translation uses TranslateForHostAt, retains cleaned
host path identities for Codex to authorize, and never uses router cwd as a
fallback. See REQ-FILE-001 and
CTR-TRANSLATE-001.
Open the router root URL, such as http://127.0.0.1:8080/, for the dashboard.
GET /api/metrics returns the process-lifetime capturer snapshot. Provider
usage is authoritative for model consumption. Metrics are auxiliary and cannot
replace a successful edit, command result, or rejection diagnostic.
Use --capture-output PATH when durable evidence is needed. The file contains
sanitized JSONL: payload sizes, statuses, provider usage, tool identities, and
bounded outcome kinds. Raw prompts, scripts, patches, credentials, and full
diagnostics are discarded after measurement.
The executable benchmark requires Docker Compose, Codex authentication, and
the task's local source under benchmarks/repos/. The default task,
etcd-range-stream, needs a local etcd checkout. Read the
benchmark methodology before running
bash benchmarks/bench.sh. Capture, snapshot shape, and comparison rules are
REQ-METRICS-001 and
REQ-BENCH-001.
| Doc | Contents |
|---|---|
doc/brief.md |
Product brief and scope |
doc/spec/index.md |
Specification inventory; each listed file owns one requirement |
doc/architecture/index.md |
Ownership-contract inventory |
doc/benchmarks.md |
Benchmark operation and interpretation |
doc/codex-router-e2e.md |
Codex-facing end-to-end procedure |
contrib/systemd/hpatch-router.service |
User service template |
contrib/codex/file-editing-instructions.md |
Persistent CTP/2, edit, shell, read, search, and inspection guidance |
AGENTS.md |
Agent workflow and repository navigation |
go generate ./internal/router/toolplugin
bun test ./internal/router/toolplugin/tests
go test ./...
go vet ./...
make installFocused checks are go test . for the engine, go test ./internal/router for
routing and plugins, and go test ./cmd/hpatch-router ./cmd/shell for the
process entry points.
Starting the router in hpatch mode with HPATCH_DIAGNOSE=1 adds the
model-visible report_issue tool. Configure hooks.diagnose in
$XDG_CONFIG_HOME/hpatch/settings.json or ~/.config/hpatch/settings.json.
See REQ-DIAGNOSE-001.