hillclimb
Use Claude Code and Codex to autonomously optimize a score you define.
You describe one problem as a single script: verifier.sh runs a candidate solution.py and writes back a number. hillclimb then spends a compute budget having headless coding agents write, debug and improve that solution.py — scoring every version through your verifier and keeping the one that scores best.
Quickstart
Install the CLI. It needs Python 3.12+ and a logged-in claude or codex to drive.
pip install hillclimb
Get the problem definition into your working directory. This creates a hillclimb/ folder and copies the bundled Heilbronn-triangle problem into hillclimb/problems/. The file verifier.sh is the only process hillclimb ever starts and verify.py is the scorer that it's using. The following command gets the Heilbronn triangle problem into your working directory (get a feel for this problem here).
hillclimb problem get heilbronn-convex-13
Start climbing. Parallelism in hillclimb has two levels: --parallel-searches is how many independent searches (exploration trees) attack the problem (each its own engine process, all in one run so they share what they learn), --parallel-operators how many coding agents each search keeps busy at once, each generating one candidate at a time running in the background.
hillclimb run heilbronn-convex-13 \
--budget 30m \
--parallel-searches 2 \
--parallel-operators 3 \
--backend claude-code \
--model claude-sonnet-5
Then run hillclimb watch for the watching the agents in action, hillclimb chart for the chart with successive progression. hillclimb stop --all ends it, and the best solution.py of every search stays in hillclimb/runs/.
What is hillclimb for?
Anything you can phrase as: a program or artifact in, a number out, and a verifier that computes that number/score (potentially using data the agents never seen). Some problem types that are well suited for hillclimb:
| Problem type | What Hillclimb improves |
|---|---|
| Optimization | Packing, routing and scheduling solutions, scored by solution quality, cost or constraint violations. |
| Prediction and forecasting | Training and prediction code, evaluated on hidden data using metrics such as MAE, pinball loss or CRPS. |
| Performance engineering | Code for a fixed workload, scored by runtime, memory use, binary size or another resource constraint. |
| Parameter fitting | Estimation code that recovers unknown parameters, tested against cases with known ground truth. |
| Strategies and policies | Dispatch, bidding and cache-eviction strategies, evaluated by replaying historical or simulated scenarios. |
| Generated artifacts | SQL queries, regular expressions, solver configurations and prompts—anything that can be generated and scored. |
| Mathematical discovery | Constructions, counterexamples and bounds, scored by a programmatically verifiable mathematical objective. |
Where it fits poorly. Verifiers that take hours: the loop needs many candidates per budget and starves. Objectives without a scalar score, like UX, prose or "nicer code". Pass/fail verifiers with no partial credit, since a 0/1 score gives the search nothing to climb. Low-dimensional continuous optimisation, where a numerical optimizer is the better tool.
Rule of thumb. Can each attempt be scored automatically in under fifteen minutes? Does the score reliably distinguish better attempts from worse ones? Does improving the score improve the outcome you actually care about? Three yeses make your problem a strong candidate for hillclimb.
Example: Heilbronn triangle full walkthrough
01Specify the problem
You specify the problem by writing the verify.py file. For the Heilbronn problem, the scorer looks something like the following:
hillclimb/problems/heilbronn-convex-13/verify.py (shortened) show
# 13 points in the plane; score = min triangle area / convex hull area N, N_TRIANGLES = 13, 286 def fail(reason): write_report(0.0, report_error=reason) # invalid configurations score 0 sys.exit(0) df = pd.read_csv("submission.csv") # id,x,y if len(df) != N or sorted(df["id"].tolist()) != list(range(N)): fail(f"need exactly {N} rows with id 0..{N - 1}") points = df.sort_values("id")[["x", "y"]].to_numpy(float) if not np.all(np.isfinite(points)): fail("coordinates must be finite") # translation and uniform scaling leave the score unchanged; normalize # so very large or very small valid coordinates stay numerically safe points = points - points[0] points = points / np.max(np.abs(points)) hull_area = polygon_area(convex_hull(points)) if hull_area <= np.finfo(float).eps: fail("degenerate convex hull") # the smallest of all C(13,3) = 286 triangles, relative to the hull min_area = min( 0.5 * abs(cross(points[i], points[j], points[k])) for i, j, k in combinations(range(N), 3) ) write_report(min_area / hull_area) # -> $HILLCLIMB_RESULT
Exiting 0 means the candidate is valid, and whatever the scorer writes to $HILLCLIMB_RESULT is its score: a bare number, or {"score": 0.0309}. The full file also reports the six smallest triangles and their areas, so an improve operator sees where the configuration is weakest, not just the final number. Tip: make the verifier hard to fool: whatever it fails to check, the search will eventually exploit.
A config file in the same folder problem.yaml names the metric, the winning direction, and any baseline scores the chart draws as reference lines:
hillclimb/problems/heilbronn-convex-13/problem.yaml
problem_id: heilbronn-convex-13 metric: normalized-min-triangle-area higher_is_better: true description: description.md baseline: baseline.py baseline_files: {submission.csv: sample_submission.csv} chart_baselines: "OpenEvolve (GPT-5, 100 candidates)": 0.0267 "AdaEvolve (GPT-5, 100 candidates)": 0.0290 "AlphaEvolve": 0.030936889034895654 time_budget_s: 1800 allow_network: false
02Give it a budget, the model and start a search
The budget is wall-clock time: hillclimb keeps drafting and scoring candidates until it is spent, and that is the only stopping rule. --backend picks the coding agent that writes the candidates (claude-code or codex) and --model the model it uses; both can be set once in hillclimb/config.yaml and left off the command. Everything a search produces, from every candidate's code to its logs and scores, lands under hillclimb/runs/.
hillclimb run heilbronn-convex-13 \ --budget 30m \ --parallel-searches 2 \ --parallel-operators 3 \ --backend claude-code \ --model claude-sonnet-5
Agents now take turns as operators: draft a new approach, debug one that crashed, improve the best scorer so far, ensemble the survivors at the end. Every new candidate runs through your verifier, and the best solution.py is kept in the search's best/ folder.
03Watch it climb
Now watch the agent work with:
hillclimb watch # the tree, live hillclimb chart # the curve below
every search of the problem, as hillclimb chart plots it: each dot is a scored candidate in landing order, the line is the best score so far across all searches, and the published scores from problem.yaml are the reference lines
04Inspect the solution
What a search leaves behind is code. best/solution.py is the winning program and best/submission.csv is what it produced. This search's winner is the draft itself: a two-phase global search that polishes structured and random starts across hull sizes 8–13 with an SLSQP solve (maximize the smallest triangle area with the hull pinned to area 1), then perturbs the leader until time runs out. Both parallel searches converged on the same configuration.
searches/heilbronn-convex-13/best/solution.py (shortened) show
"""Multi-start global search over diverse hull/interior topologies for the normalized Heilbronn problem (13 points), each candidate polished with an epigraph SLSQP formulation (maximize t = min triangle area subject to a fixed convex-hull area of 1), keeping the best true-scored configuration found. """ def true_score(pts): area, hull_idx = hull_area_and_indices(pts) # scipy ConvexHull tri = triangle_areas(pts) # all 286 of them return float(tri.min() / area) def polish(pts0, maxiter=150): # pin every triangle's orientation, then push up the smallest # signed area as an epigraph variable t pts_g, hull_idx = gauge_normalize(pts0) # hull area -> 1 signs = np.sign(triangle_crosses(pts_g)) def tri_cons(x): pts, t = unpack(x) return signs * triangle_crosses(pts) * 0.5 - t # area_i >= t res = minimize(neg_t, x_init, jac=neg_t_grad, method="SLSQP", constraints=[{"type": "eq", "fun": hull_eq}, {"type": "ineq", "fun": tri_cons}], options={"maxiter": maxiter, "ftol": 1e-14}) return unpack(res.x)[0] def main(): best_pts = polish_iterated(REF_PTS, rounds=3) # known-good seed best_score = true_score(best_pts) # phase 1: broad exploration across hull/interior topologies for kind, hull_size in starters: # hull sizes 8..13 if time.time() > phase1_deadline: break pts = polish_iterated(make_start(kind, hull_size), rounds=2) if pts is not None and true_score(pts) > best_score: best_pts, best_score = pts, true_score(pts) # phase 2: basin-hopping local search around the current best while time.time() - start < TIME_BUDGET_S: pts = polish_iterated(perturb(best_pts), rounds=2) if pts is not None and true_score(pts) > best_score: best_pts, best_score = pts, true_score(pts) # final tight refinement of the winner, then write it out best_pts = polish_iterated(best_pts, rounds=4, maxiter=400) write_csv("submission.csv", best_pts)
its submission.csv, drawn: 13 points, ten on the hull and three interior. The shaded triangle is the smallest of the 286; its area over the hull's is 0.0309372, where AlphaEvolve reported 0.0309369
05See what it learns
Every search writes what it learned into a graph: claims, techniques, libraries, and the problems it ran on. Later searches read it back, so the next one does not start from nothing. Below is an example of a hillclimbrun solving the Heilbronn problem. Slide back through time and each search is a tick where new claims appear.
drag rotate · shift-drag pan · scroll zoom · click a type to hide · slide to scrub time
How to make it hard to cheat?
Agents optimize the score your verifier produces, not necessarily the outcome you intended. If there is a shortcut or loophole, the search may find it.
Test on cases the agent never sees. Score a solver rather than a fixed answer, and evaluate it across multiple instances. Keep some instances hidden with holdout: true so the agent must discover a solution that generalizes instead of memorizing the visible cases.
Make improvements real and reproducible. Score invalid outputs as zero, avoid unnecessary rounding, and measure the verifier’s noise with hillclimb verify --repeat 5. Set search.min_improvement above the noise floor so Hillclimb only accepts meaningful improvements—and inspect the winning code before using it.
Installation
hillclimb needs Python 3.12+ and a coding agent it can drive. Install the CLI from PyPI:
pip install hillclimb # or: uv add hillclimb
Operators run as headless claude or codex processes, so the backend you pick has to be installed and logged in.
Form factor
hillclimb is a CLI that drives coding agents in headless mode. The engine itself is the loop that runs, scores, journals and selects, and it has three seams. A SearchPolicy decides what to try next: the engine hands it a read-only view of the search and gets back one action. An OperatorBackend executes that action by running one headless subagent per operator call. A DataStore holds the records the loop writes and every view reads back: the journal, status and the stop/prune queue. Files in a folder by default, or a SQLite database if you prefer that.
Contribute
hillclimb has three seams, and each one is a small protocol: how to search, who writes the code, and where the records live. Implement one and it plugs in.
01A search policy
Decides what to try next: one action at a time over a read-only view of the search. Everything else (prompts, agent calls, trials, journaling) stays with the harness.
src/hillclimb/policy.py show
class SearchPolicy(Protocol): name: str params: dict # one Action -- draft | debug | improve | ensemble -- # or None to hold the slot open def propose(self, view: SearchView) -> Action | None: ... # every terminal result, and replayed on resume def observe(self, view: SearchView, candidate: Candidate) -> None: ...
greedy is the only one so far, at 170 lines. Beam search, MCTS, evolutionary populations or a bandit over operators all fit this shape. Add yours to the registry in policies/__init__.py and run it with hillclimb run --policy yours.
02An agent backend
Runs one headless coding agent over a prompt and a working directory, and reports what happened.
src/hillclimb/backends/base.py show
class OperatorBackend(Protocol): name: str # take a prompt and a working directory, write code, report what happened def invoke(self, request: OperatorRequest) -> OperatorResult: ...
claude-code and codex are supported today; dummy runs the engine with no model calls at all. Future integration could include OpenCode, Pi and friends.
03A data store
Holds a search's records: metadata, the append-only candidate journal, the status heartbeat, and the stop/prune queue. Candidate code and agent logs stay on disk either way.
src/hillclimb/store.py show
class DataStore(Protocol): # upserts for runs, searches and status; appends for the journal; # a consume-once queue for commands def record_run(self, meta: RunMeta) -> None: ... def record_search(self, meta: SearchMeta) -> None: ... def runs(self) -> list[RunMeta]: ... def searches(self, problem_key=None, run_id=None) -> list[SearchRecord]: ... def search(self, key: SearchKey) -> SearchRecord | None: ... def journal(self, key: SearchKey) -> JournalBackend: ... def write_status(self, key: SearchKey, status: SearchStatus) -> None: ... def read_status(self, key: SearchKey) -> SearchStatus | None: ... def enqueue_command(self, key: SearchKey, cmd: ControlCommand) -> None: ... def drain_commands(self, key: SearchKey) -> list[ControlCommand]: ... def clear_stale_stops(self, key: SearchKey) -> None: ... def close(self) -> None: ...
files is the zero-setup default; sqlite (store.backend: sqlite) puts the same records in one database file, safe for many engines writing at once.
Suggest new policies, backends and stores as pull requests: the registries are plain dicts and the store is a config key. The shortest complete examples are greedy.py, dummy.py and store.py's FileDataStore.
Licence
hillclimb is free (free both as in freedom and "free beer") software under the MIT License.