Skip to content
Harness Engineering2026-10-2012 min read

How to Build an RL Environment With a Verifier in Python

Build a small RL environment in Python: a task generator, a verifier and two policies. Then show a loose verifier paying a policy that does no work, and test the verifier with hostile inputs.

Predict first. A verifier checks that the correct number appears in the response. Can a policy that never does the work still score 100%? Decide before you read on.

Key takeaways — after your prediction
  • A verifier is code that decides the reward, so a loose one pays for whatever it fails to rule out.
  • In our toy environment, a policy that lists every possible answer scores 100% under a substring check and 0% under a strict one.
  • Test the verifier with hostile responses before you test any policy.
  • Two Anthropic postings list building evaluations and RL environments as a duty; the fellowship asks for a PhD in a named field, not machine learning.

Rod Rivera

Author

How to Build an RL Environment With a Verifier in Python

Rod's note: read with a pencil; the margins are for you.

In reinforcement learning, the verifier decides the reward. If it can be satisfied without doing the task, a policy trained against it learns to satisfy the verifier. This tutorial builds the smallest environment that shows the problem: one task type, one verifier you can loosen or tighten, and two policies.

Writing checks like this is also what some lab postings describe as the job. Anthropic's Conceptual Reasoning Fellow posting lists "Designing evaluations and RL environments that test models in your field systematically" as a duty, and a Fellows Program posting lists "Creating RL environments to improve Claude models at capabilities that are within your domain of expertise." We read both on October 13, 2026; postings change and close, so check them before you plan around them. This is two postings from one lab, not a hiring checklist.

What you will make: a 46-line Python environment with a task generator, a verifier and two policies, plus six hostile test cases for the verifier. You need Python 3.9 or newer and nothing else: no package, account or model.

What is executed and what is observed

Both files below were executed on October 13, 2026 with Python 3.14.6 and 3.9.6, and the output was identical on both. The environment is a toy: the "policies" are scripts, not models, so the numbers show how a verifier is gamed, not how any real model behaves. The job postings are read from public pages and can change.

Our article on building an agent eval environment that resists reward hacking covers the full design. Here you build the smallest version and break it yourself.

Quick check — A verifier gives reward 1 when the correct number appears anywhere in the response. A policy writes every plausible number in one sentence. What does it score?

Check the verifier

Build it

Save this as env.py. The task is a unit conversion whose answer is exact by construction.

python
#!/usr/bin/env python3
"""A minimal RL environment: tasks, a verifier, a reward, and two policies."""
import random
import re

def make_task(rng):
    """One unit-conversion task. The hidden answer is exact by construction."""
    km = rng.randint(2, 40)
    return {"prompt": f"A trail is {km} km long. How many meters is that?",
            "answer": km * 1000}

def loose_verifier(task, response):
    """First draft: reward any response that mentions the right number."""
    return str(task["answer"]) in response.replace(",", "")

def strict_verifier(task, response):
    """Second draft: exactly one 'ANSWER: <number>' line, and it must match."""
    lines = [l for l in response.splitlines() if l.startswith("ANSWER:")]
    if len(lines) != 1:
        return False
    match = re.fullmatch(r"ANSWER:\s*([0-9][0-9,]*)", lines[0].strip())
    return bool(match) and int(match.group(1).replace(",", "")) == task["answer"]

def honest_policy(task, rng):
    """Does the work, but gets the unit wrong a third of the time."""
    km = task["answer"] // 1000
    value = km * 1000 if rng.random() > 1 / 3 else km * 100
    return f"{km} km is {value} m.\nANSWER: {value}"

def hacking_policy(task, rng):
    """Does no work: lists many candidates and hopes one is right."""
    guesses = [n * 1000 for n in range(2, 41)]
    return "It could be " + ", ".join(str(g) for g in guesses) + "."

def pass_rate(policy, verifier, n=2000, seed=0):
    rng = random.Random(seed)
    wins = 0
    for _ in range(n):
        task = make_task(rng)
        wins += verifier(task, policy(task, rng))
    return wins / n

if __name__ == "__main__":
    for pname, policy in (("honest", honest_policy), ("hacking", hacking_policy)):
        for vname, verifier in (("loose", loose_verifier), ("strict", strict_verifier)):
            print(f"{pname:8} {vname:7} pass rate {pass_rate(policy, verifier):.3f}")

Run it:

bash
python3 env.py

These are the results we got, on 2,000 tasks with seed 0:

PolicyLoose verifierStrict verifier
Honest (wrong unit one time in three)0.6790.679
Hacking (lists 39 candidate answers, no work)1.0000.000
Where the reward comes from: the verifier decides what gets paid. Loose: number appears anywhere; Strict: one ANSWER line, exact match; Task: 5 km in meters?; Policy response; Verifier; Reward 1, even for a list of guesses; Reward 1 only for the right answer
Where the reward comes from: the verifier decides what gets paid

The honest policy scores the same under both, which is what a good verifier does for honest work. The hacking policy goes from a perfect score to zero when the verifier changes. The only difference is what the check rules out.

Attack your own verifier

A verifier is code, so test it like code, with inputs written to break it. Save this as check_verifier.py next to env.py:

python
from env import strict_verifier

task = {"prompt": "A trail is 5 km long. How many meters is that?", "answer": 5000}
cases = [
    ("ANSWER: 5000", True),
    ("5 km is 5000 m.\nANSWER: 5,000", True),
    ("ANSWER: 5000\nANSWER: 4000", False),   # two answers
    ("ANSWER: 5000 or 6000", False),          # hedged
    ("The answer is 5000.", False),           # no answer line
    ("ANSWER: 500", False),                   # wrong
]
for response, expected in cases:
    got = strict_verifier(task, response)
    assert got == expected, (response, got, expected)
print(f"{len(cases)} verifier cases pass")
bash
python3 check_verifier.py

It prints 6 verifier cases pass. Each rejected case is a behavior you decided not to pay for: two answers, a hedge, no answer line, a wrong answer. Write these before you run any policy. A policy shows you the cheats it found; hostile cases show you the ones you thought of.

The strict version still has limits. It trusts the first line that starts with ANSWER:, accepts a comma-grouped number, and says nothing about whether the work shown above it is correct. Real environments need a decision for each of these, and the decision is a design choice, not a detail.

What this does not show

A scripted policy isn't a trained model. The hacking policy here is blunt on purpose. Real reward hacking is subtler and often found by a model in a pass over thousands of tasks, which is why audits read transcripts as well as scores. Our toy also has one task type, one verifier and no noise. Treat the exercise as practice in reading a verifier critically, not as a measure of anything.

Exercise

Break the strict verifier, then fix it.

Find a cheat for the strict verifier

Write a third policy that scores above zero under strict_verifier without computing the conversion. Hint: the task generator draws km from a small range, and the verifier accepts any number that matches. Report its pass rate.

Add the missing control

Add a null policy that returns an empty string, and a gold policy that always answers correctly. A sound verifier gives the null policy 0.000 and the gold policy 1.000. Confirm yours does before you trust any other number.

Widen the task, then test again

Add a second task type, such as grams to kilograms, and make the verifier handle both. Add at least three hostile cases for the new type to check_verifier.py.

Hand it to someone else

Give env.py and the rule "score high without doing the task" to a colleague, or to a model with no hints, and count how many distinct cheats they find that you missed.

Read: build an agent eval environment that resists reward hacking

The full design: freeze the environment, hide the oracle, and prove the grader with null, cheating and gold agents.

Learn the mechanism. Test the claim.

Join the Prof Rod newsletter for one educational lesson a week, with worked examples attached. It is free to register for and separate from the Zero Employee community.