Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

115 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hpatch

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

Why hpatch?

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
Loading

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.

Requirements

  • Go 1.26 or newer (go install only; no clone required for normal use)
  • For the router: Codex CLI with ChatGPT file auth (codex login, credentials at ~/.codex/auth.json or $CODEX_HOME/auth.json)

Codex router (systemd user service)

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.

Install the binary

go install github.com/yusing/hpatch/cmd/hpatch-router@latest

Default install path is ~/go/bin/hpatch-router (or $GOBIN/hpatch-router if GOBIN is set). Ensure that directory is on your PATH.

Install and start the unit

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.service

Optional: 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:8080

If 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:9090

Then systemctl --user daemon-reload && systemctl --user restart hpatch-router.service.

Point Codex at the router

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 = true

Make 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 --oss

Profiles 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 dashboard

Override base instructions (prefer hpatch over apply_patch)

The 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.

  1. Fetch a recent copy of the Codex default base prompt (keep the rest of the file; only replace the file-editing section):

  2. In the file-editing section (often ## File editing or ## File editing constraints), replace only the apply_patch guidance with contrib/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.

  3. 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.

Standalone CLI

Install the CLI (requires Go 1.26+; binary lands in $(go env GOPATH)/bin or $GOBIN):

go install github.com/yusing/hpatch/cmd/hpatch@latest

Apply 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"
EOF

Translate 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"
EOF

Script 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 --version

Editing language (summary)

Authoritative 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:

  1. Complete logical line: LINE:HASH
  2. Inclusive complete-line range: LINE:HASH..LINE:HASH
  3. 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 type to replace, type- to insert before, type+ to insert after, and del to 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 in of 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 <<PATCH values 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 type over 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 \n when an insertion must form a new line. Reserve fixed <<PATCH for multiline or escape-heavy values.
  • Rejection changes nothing. Router-owned retries can replace, delete, or insert failed commands by index; for a fixed <<PATCH value, they can address one physical body row as COMMAND.ROW without 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

Metrics

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 calls tools.apply_patch with 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 gain

These 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 ./compare

End-to-end benchmark

The 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.

How it works

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.

Project structure

.
├── 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.

Documentation

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.

Development

git clone https://github.com/yusing/hpatch.git
cd hpatch
go test ./...
go vet ./...
go install ./cmd/hpatch ./cmd/hpatch-router

About

Compact, editor-like file edits for agents: select text, type the change, skip rewriting full patches.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages