A Codex Responses router that lets agents edit with verified target-bearing mutations instead of emitting full patches.
hpatch-router sits between Codex and the Responses API. It replaces the Code Mode apply_patch surface with constrained functions.hpatch, resolves successful scripts against the declared workspace, and hands Codex a real apply_patch carrier so sandbox checks and the normal diff UI remain intact. The repository also includes the standalone hpatch CLI and reusable Go engine used by the router.
TL;DR:
| Goal | Start here |
|---|---|
| Route Codex edits through hpatch | Install and configure the Codex router |
| Make Codex prefer hpatch | Override the base instructions |
| Inspect measured token savings | Metrics, then run hpatch gain |
| Use the engine without Codex | Standalone CLI |
| Read the full editing contract | hpatch --help, hpatch --tool-help, doc/spec/interface.md |
A direct Code Mode edit makes the model repeat patch framing, old context, replacement text, and the 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. Savings are the difference between the two model-output payload estimates shown above. State reports, rejection diagnostics, and the net cost of installing the hpatch and hread tool definitions while removing the Code Mode apply_patch section are tracked separately as input overhead. Hread results are not compared with a hypothetical cat; the dashboard's end-to-end Responses and session usage totals are authoritative for their model-input cost. Gain values remain reproducible GPT-5 estimates rather than provider billing totals.
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
The same edit through direct apply_patch in Code Mode:
functions.exec
const result = await tools.apply_patch(`*** Begin Patch
*** Update File: parser.go
@@
-func parse(input []byte) (Document, error) {
- tokens := tokenize(input)
- if len(tokens) == 0 {
- return Document{}, errEmptyInput
- }
- document := buildDocument(tokens)
- if document.Empty() {
- return Document{}, errEmptyDocument
- }
- return document, nil
-}
+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
+}
*** End Patch
`);
text(result);
The direct call 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.
The router also supplies functions.hpatch to the provider with a Lark grammar. As the model writes the tool call, only tokens that can still lead to a valid script are allowed. Bad syntax never becomes a finished tool call, so there is no generate-reject-retry cycle for it. Grammar is syntax only: a valid script can still fail for missing files, missing or stale rows, incomplete literal targets, or conflicting edits, and those failures stay atomic.
- Go 1.26 or newer (
go installonly; no clone required for normal use) - For the router: Codex CLI with ChatGPT file auth (
codex login, credentials at~/.codex/auth.jsonor$CODEX_HOME/auth.json)
The router listens on HTTP, rewrites Responses traffic so Codex calls functions.hpatch or functions.hread, evaluates scripts against the workspace declared in x-codex-turn-metadata, and returns client-executed Code Mode carriers. Hpatch produces a real apply_patch call, so you see the normal diff rather than a silent file rewrite. Hread resolves through one process-scoped temporary wrapper invoked by absolute path in the nested exec command; the carrier does not override the exec environment or working directory. The worker reads from Codex's exec working directory under Codex's sandbox and permissions rather than receiving a router workspace capability. When the router and Codex executor have isolated filesystems, deployment must expose the wrapper directory and router executable at the same absolute paths in both environments. Background Responses requests are rejected before forwarding because the router does not expose the retrieval and cancellation endpoints required to complete them.
On each request it strips the Code Mode ### apply_patch section from the functions.exec / additional_tools description and installs standalone functions.hpatch and functions.hread tools. A request with only a direct apply_patch carrier is rejected before forwarding because it cannot execute hread safely. The rewrite changes only the tool definitions the model receives for that turn; translated history still uses the Code Mode exec carrier that Codex actually runs.
Defaults:
| Setting | Default |
|---|---|
| Listen | 127.0.0.1:8080 (--listen) |
| Upstream start timeout | 10m (--timeout) |
| Auth | ~/.codex/auth.json, or $CODEX_HOME/auth.json |
| Metrics / hooks | $XDG_CONFIG_HOME/hpatch or ~/.config/hpatch |
| Endpoints | POST /v1/responses, GET / (dashboard), GET /api/metrics |
The process must run as your login user so it can open the absolute workspace paths Codex sends and read your Codex credentials. A user systemd unit is the intended long-running setup.
go install github.com/yusing/hpatch/cmd/hpatch-router@latestDefault install path is ~/go/bin/hpatch-router (or $GOBIN/hpatch-router if GOBIN is set). Ensure that directory is on your PATH.
The unit template is published at
contrib/systemd/hpatch-router.service
(raw URL works after the repo is on GitHub):
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 (or another Codex profile config under ~/.codex/):
[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 pick it per invocation (same pattern as other local providers):
codex --local-provider hpatch --ossProfiles work the same way: put the [model_providers.*] block in the profile config (or the main config), then run with --profile <name> --local-provider <provider> --oss.
Start sessions from a git worktree: the router requires exactly one usable absolute workspace root in turn metadata.
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
# open http://127.0.0.1:8080/ for the local dashboardThe router exposes functions.hpatch and strips apply_patch from the Code Mode functions.exec definition, but Codex’s default base prompt still tells the model to use apply_patch for local edits, and a runtime ALL_TOOLS dump can still list tools.apply_patch. Point Codex at a custom base-instructions file and replace that section so the model prefers hpatch even when it can see both.
-
Fetch a recent copy of the Codex default base prompt (keep the rest of the file; only replace the file-editing section):
-
In the file-editing section (often
## File editingor## File editing constraints), replace only theapply_patchguidance withcontrib/codex/file-editing-instructions.md. Leave dirty-worktree handling and the non-destructive git rules as in the default base prompt; those are not hpatch-specific. -
Point Codex at your file in
~/.codex/config.toml(or a profile config):
model_instructions_file = "/absolute/path/to/your/base_instructions.md"Do not rely on project AGENTS.md alone for this: the stock base prompt still steers file edits toward apply_patch. Override the base prompt the same way other host tooling (for example skills) does when it must replace a default section rather than append to AGENTS.md.
Install the CLI (requires Go 1.26+; binary lands in $(go env GOPATH)/bin or $GOBIN):
go install github.com/yusing/hpatch/cmd/hpatch@latestApply a script using a hash copied from hread or an earlier hpatch report (writes only after the full script validates and stages; success report on stderr):
hpatch <<'EOF'
in src/app.go
type 12:55af "oldName" "newName"
EOFTranslate to an OpenAI apply_patch envelope without touching files (patch on stdout, pending report on stderr):
hpatch translate <<'EOF'
new message.txt
type "hello world\n"
EOFScript paths are workspace-relative.
| Surface | Workspace root | Path base inside root |
|---|---|---|
| Standalone CLI | Process current directory, or absolute --root |
., or --cwd (relative or absolute, must stay under root) |
| Codex router | The single usable absolute path from x-codex-turn-metadata |
Root itself (no CLI flags) |
Translated patches always use root-relative paths. Details: hpatch --help.
| Mode | Mutates files? | stdout | stderr |
|---|---|---|---|
hpatch |
Yes, after full validation | empty on success | final-state report |
hpatch translate |
No | apply_patch envelope |
pending final-state report |
hpatch gain |
No | metrics report | empty on success |
--help / --tool-help / --version |
No | help or version | empty |
Built-in references:
hpatch --help
hpatch --tool-help
hpatch translate --help
hpatch --versionAuthoritative guidance: hpatch --help and hpatch --tool-help. Contract: doc/spec/interface.md.
Hread and hpatch preview/context rows have the shape LINE:HASH TEXT. Copy the complete
LINE:HASH reference into a mutation target. The one-based line selects the exact logical
line; the four-digit lowercase hash rejects stale content, including changed indentation.
Targets:
- Complete logical line:
LINE:HASH - Inclusive complete-line range:
LINE:HASH..LINE:HASH - Exact literal occurrence(s) from a verified row through EOF:
LINE:HASH "TEXT" [COUNT]
Rows verify only their named immutable-baseline line. Hpatch does not scan for a matching hash elsewhere, so equal lines at different positions are unambiguous. A text target starts at its verified row and every requested non-overlapping match must exist.
Commands are in / new / mv / rm, target-bearing type / type- / type+ /
del, and one targetless type VALUE immediately after new.
Rules worth remembering:
- Use
typeto replace,type-to insert before,type+to insert after, anddelto delete. - Use search to locate likely edit regions, then use HREAD for their first content read; issue independent HREAD calls together and use only current rows from the exact path.
- First
inof a file freezes its immutable invocation baseline. Pending edits never shift later targets. - Batch short, disjoint edits across inspected files when they are expected to validate or fail together. Keep unrelated large
<<PATCHvalues in separate calls, with at most one syntax-sensitive multiline Go declaration or function replacement per call; short supporting edits for that same change may remain with it. - For an existing Go declaration or function, prefer one range
typeover assembling the same replacement through several insertions. After success touches a file, discard its saved references and HREAD it again before another edit. - Overlapping replacements or deletions and insertions strictly inside them fail atomically. Boundary insertions are valid.
- Use inline quoted values for short single-line edits; include
\nwhen an insertion must form a new line. Reserve fixed<<PATCHfor multiline or escape-heavy values. - Rejection changes nothing. Router-owned retries can replace, delete, or insert failed commands by index; for a fixed
<<PATCHvalue, they can address one physical body row asCOMMAND.ROWwithout resending the large value.
Multiline example:
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
Successful CLI and router edits record paired GPT-5 output-token estimates for:
- hpatch:
functions.hpatch+ the model-emitted script - baseline:
functions.exec+ a fixed program that callstools.apply_patchwith the translated envelope
Failed calls charge the full rejected hpatch payload against an empty-patch baseline. Final-state reports, diagnostics, and once-per-session tool definitions are tracked as input overhead separately.
hpatch gainThese are reproducible payload estimates, not provider billing totals. They omit reasoning tokens, commentary, and host-specific framing.
Hand-authored scenario comparison (does not update hpatch gain):
go run ./compareThe paired benchmark runs one stock Codex control attempt and one Hpatch attempt
from independent copies of the same historical etcd base revision, alternating
which arm runs first. Hidden executable tests and an allowed-path boundary grade
correctness before timing or token-efficiency differences are considered. The
active task, etcd-range-stream, reconstructs etcd's cross-layer server-side
RangeStream behavior. See the benchmark methodology and the
latest published result.
That two-repetition gpt-5.6-sol run passed both arms 2/2 and reported 43.0%
lower successful edit payload for Hpatch (4,887 tokens versus 8,576
control-equivalent tokens). It is one observed run, not a general performance
guarantee.
CLI: resolve workspace (--root / --cwd or process cwd) → parse script → verify targets against immutable baselines → plan and render disjoint splices → stage the multi-file result → commit (normal mode) or emit one apply_patch envelope (translate).
Router: load ChatGPT Codex auth → accept POST /v1/responses → require a Code Mode exec owner and expose functions.hpatch plus functions.hread instead of its nested apply_patch → translate hpatch against the single usable workspace from Codex metadata or route hread through the process wrapper in Codex's exec context → return an exec carrier that applies the real patch or returns the exact read result.
Hpatch workspace selection is host-owned, and zero or multiple usable roots fail closed. Codex enforces sandbox and filesystem permissions for the client-executed hread and apply operations.
.
├── cmd/
│ ├── hpatch/ # CLI entry point and command-line contract
│ └── hpatch-router/ # Router process entry point
├── internal/
│ ├── router/ # Responses proxy, hpatch translation, auth, metrics, and dashboard
│ ├── hpatchsyntax/ # Shared quoted-string and heredoc parsing
│ └── patchtest/ # Test helper for applying translated patch envelopes
├── compare/ # Hand-authored hpatch vs. apply_patch token scenarios
├── contrib/ # Codex prompt guidance and systemd service template
├── doc/ # Product brief, interface specification, and architecture index
├── *.go # Core parser, editor, workspace, transaction, translation, hooks, and metrics
├── tool_description.md # Embedded function-tool instructions
└── tool_grammar.lark # Embedded constrained-decoding grammar
Tests live beside the packages they exercise. The root hpatch package is the reusable engine; cmd/hpatch and internal/router call it rather than maintaining separate editing implementations. The router dashboard is embedded from internal/router/dashboard.html.
| Doc | Contents |
|---|---|
doc/brief.md |
Product brief and scope |
doc/spec/index.md |
Spec inventory |
doc/spec/interface.md |
CLI, script, correction, metrics contracts |
doc/spec/comparison.md |
Token comparison scenarios |
doc/architecture/index.md |
Architecture ownership |
contrib/systemd/hpatch-router.service |
User unit template |
contrib/codex/file-editing-instructions.md |
Base-prompt replacement for file editing |
AGENTS.md |
Codex router E2E notes for agents |
Library use: module path github.com/yusing/hpatch. Importable as a library (hpatch.Translate, hpatch.Workspace, host metrics helpers); hosts should open an *os.Root capability for the workspace before calling in.
git clone https://github.com/yusing/hpatch.git
cd hpatch
go test ./...
go vet ./...
go install ./cmd/hpatch ./cmd/hpatch-router