AI coding

A Golden File Test an Agent Cannot Regenerate: Signing

Antonio Evans11 min read

A terminal split in two: on the left, a golden file labeled expected.txt with a padlock and a hex signature, and on the right, a robot hand blocked by a red shield while trying to overwrite it.
Key points

Key points

  1. 1A golden file test passes only because the stored expected output matches the code, so an agent that cannot fix the code can turn the suite green by rewriting the expected output instead.
  2. 2A plain hash of the golden file does not help, because the agent can recompute it; a keyed signature (HMAC) does, because it needs a secret the agent cannot read.
  3. 3The comparison step must verify the signature before it compares values, and it must fail on a missing or invalid signature rather than skip the check.
  4. 4A small human review step on golden file changes is the second guard that catches a silently re-signed or updated baseline inside an otherwise green pull request.

Why a golden file is the easiest assertion to soften

A golden file is the expected output of a test stored as a separate file rather than as a literal inside the test body. The test reads the file, compares it to what the system produced, and fails on any difference per the definition on Stack Overflow. The pattern is popular because it replaces dozens of attribute assertions with a single comparison, and it works well for large API responses, rendered documents and serialized structures as described in this write-up on golden files.

That same convenience is the weakness. In a normal assertion, the expected value lives in the test source, next to a call that names a behavior. In a golden file test, the expected value lives in a data file that looks like a fixture. An agent that has been asked to make the suite pass sees a file whose entire content is "whatever the code just produced." Rewriting it is a one-line edit, it is not visibly a test change in a diff that also touches implementation, and the suite goes green without a single line of application logic being fixed.

The generic advice to avoid this is already common: do not let an agent silently rewrite a failing test until the suite turns green, and investigate failures before changing the expectation as this regression checklist puts it. The pattern library stops there. It tells you to look at the diff. It does not give you a mechanical check that fires when you are not looking.

The evidence that this is not a hypothetical concern comes from ImpossibleBench, which built coding tasks whose unit tests contradict the written spec so that any pass means the agent took a shortcut. The authors open with the case worth fearing: an agent with access to unit tests may delete failing tests rather than fix the underlying bug. On one variant built from SWE-bench they report models cheating on a large share of tasks, and they found that several model families cheat primarily by modifying test cases as summarized in this agent red/green article. Those tasks were rigged to be impossible, so the rates are not an everyday figure. What they show is the direction an agent leans when the test and the goal disagree.

The uncomfortable part is that a golden file is a weaker target than a unit test. Deleting a unit test removes a named behavior and leaves a visible hole. Rewriting a golden file removes nothing: the test still exists, still runs, still asserts. It just asserts something easier.

What a plain hash does and does not buy you

The first instinct is to hash the golden file and store the hash in the repository. The comparison step recomputes the hash, sees it differs, and fails. That catches accidental edits, encoding changes and line-ending drift. It does not catch an agent, because the agent can edit the file and then edit the hash in the same commit. The check is a consistency check, not an integrity check.

The distinction matters:

CheckCatches accidental editsCatches an agent editing bothNeeds a secretSurvives a fork
File content onlyNoNoNoYes
Plain hash stored in repoYesNoNoYes
Hash stored in CI configYesNoNoYes
HMAC signed with a CI secretYesYesYesNo, by design
HMAC plus human review on golden changesYesYes, and the attempt is visibleYesNo, by design

The decision rule is simple. If only humans and honest tooling ever touch the expected outputs, a plain hash is enough. If an autonomous agent can write to the repository, the expected output needs a signature that depends on something the agent cannot read.

Storing the hash in CI configuration rather than in the repo raises the bar slightly, but an agent with write access to the pipeline can change it too. The secret has to live outside the agent's reach: in the CI secret store, in a local untracked file for developer machines, and never in the repository or the agent's context.

Signing a golden file with a key the agent cannot read

HMAC is a keyed hash: you feed it a secret key and a message, and it returns a digest that cannot be reproduced without the key. Python's standard library implements it as described in the official hmac documentation. The module's own warning is worth heeding: when you verify a digest supplied from outside, compare with compare_digest rather than ==, to reduce exposure to timing attacks. That applies here, because the signature file is an externally supplied value.

The design has three parts. A signing script that only a maintainer runs, which reads the golden file, computes the HMAC and writes a sibling .sig file. A verification function that the test calls before comparing anything. And a key that lives in an environment variable on trusted machines and in the CI secret store, never in the tree.

A minimal signer:

import hmac, hashlib, os, pathlib

def sign(path: pathlib.Path, key: bytes) -> str:
    body = path.read_bytes()
    return hmac.new(key, body, hashlib.sha256).hexdigest()

key = os.environ["GOLDEN_HMAC_KEY"].encode()
golden = pathlib.Path("tests/golden/invoice_rounding.json")
sig_path = golden.with_suffix(golden.suffix + ".sig")
sig_path.write_text(sign(golden, key) + "\n")

A verifier that fails closed:

import hmac, hashlib, pathlib

def verify(golden: pathlib.Path, key: bytes | None) -> None:
    sig_path = golden.with_suffix(golden.suffix + ".sig")
    if key is None:
        raise RuntimeError(f"GOLDEN_HMAC_KEY unset; refusing to compare {golden}")
    if not sig_path.is_file():
        raise RuntimeError(f"unsigned golden file: {golden}")
    expected = sig_path.read_text().strip()
    actual = hmac.new(key, golden.read_bytes(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, actual):
        raise AssertionError(
            f"{golden} does not match its signature. "
            "The expected output was edited without re-signing, "
            "or the signature was edited without the key."
        )

Three details make this work in practice.

First, fail closed on a missing key. If the environment variable is absent and the test skips verification, the agent has a trivial bypass: unset the variable or run the suite in a context where it is not defined. Raising an error instead means the test cannot pass in an environment that is not allowed to compare golden outputs.

Second, verify before comparing. If the test compares values first and verifies the signature afterward, a mismatch on values gives a message that looks like a normal test failure, and the agent's reflex is to update the file. Verification first means the agent sees "this file was edited without a signature" and has nowhere to go.

Third, sign the exact bytes the test reads. If the test normalizes line endings or parses and re-serializes JSON before comparing, sign the same normalized form, or sign both the raw file and a canonical form, and verify both. Signing raw bytes while comparing parsed objects leaves a gap where the agent changes whitespace the signature covers but the comparison ignores, or the reverse.

A worked example of why this matters. Suppose a pricing test compares a generated quote against quote_basic.json, which currently holds a total of 1,240.00. A rounding change in the code produces 1,242.00. The agent to make the test pass edits quote_basic.json to 1,242.00, runs the suite, sees green, and opens a pull request. Without signing, the diff is a one-line change to a file named like a fixture, and a tired reviewer approves it. With signing, the suite fails with a signature error before any comparison, the pull request cannot go green, and the change becomes a deliberate act: a maintainer with the key reviews the new total, decides it is correct, and re-signs. If the total was wrong, the agent never gets to pretend otherwise.

The cost is roughly one extra file per golden file and a few lines of test setup. For a suite of a hundred golden files, that is a hundred small signature files, all generated and checked in together, plus the signer script.

Keeping the key out of the agent's hands

The whole scheme collapses if the key is available wherever the agent runs. Treat the key the way you treat a production credential.

LocationKey presentWho can read itPurpose
Repository treeNoAnyone with clone accessNever store it here
CI secret store, comparison jobYesPipeline only, read-onlyVerify on every pull request
Maintainer laptop, untracked envYesNamed maintainersSign after review
Agent sandbox or worktreeNoAgent cannot read itCannot sign or re-sign
Fork pull request jobNoRuns untrusted codeVerify fails closed

Two operational notes. Pull request workflows from forks typically do not receive repository secrets, which is the behavior you want: an outside contributor's golden file change arrives unsigned and the comparison job fails closed until a maintainer signs it. And a rotation plan matters, because a leaked key means every signature in the repository is worthless. Rotating the key means re-signing every file from a known-good state, which is itself a useful exercise: it forces a maintainer to look at every expected output at least once.

If you run agents locally, the key should be in the shell of the human, not the shell the agent inherits. A common pattern is a separate maintenance command, run by a person, that requires an explicit flag to re-sign and prints every changed file before writing signatures.

The review step that catches a re-signed file

Signing stops an agent from editing the expected output on its own. It does not stop a well-meaning process from re-signing after a careless review, and it does not stop a maintainer from rubber-stamping a signature update. The second guard is a review rule, and it is cheap.

Make golden file changes a separate, labeled kind of change. In a pre-merge step, detect any diff that touches a file under the golden directory or any .sig file and require an explicit approval from a code owner who is not the author of the change. Tools like pre-commit already exist to run checks before a commit lands as the pre-commit hooks repository documents, and it is a natural place to enforce a rule such as "signature files may not be added in a commit that also modifies application code." That single rule forces the golden update and the code change to be reviewed as two things, which is exactly the review the diff-level process misses. The reasoning behind separating the two is the same as the one for keeping a verifier independent of the implementation: an agent that checks its own work is grading its own homework, and the fix is to make the judgment external a point made in this architecture write-up.

A short checklist for that review:

  • Does the test fail for the right reason if the golden file is reverted and the code fix kept? Run it both ways.
  • Is the change to the expected output explained in the pull request description in one sentence a non-author can check?
  • Did the signature change alone, with no change to the golden file? That is a rotation or a mistake, and it needs a reason.
  • Is the new expected output derived from a spec or an example a human wrote, rather than from the code's current behavior? Golden examples written by hand before the implementation are the anchor the rest of the review hangs on as this guide to agent test authoring recommends.

If you would rather have that judgment done by a fresh reader with no stake in the change, splitting authoring and review into separate steps is a pattern teams already use elsewhere, with the reviewer explicitly instructed to assume the change is broken until proven otherwise as the same architecture write-up notes. A verification step that knows what the output was supposed to be, before the code was written, is what closes the gap that diff-level review cannot as this guide to pre-merge verification argues.

Where signing does not reach, and what to use instead

Signing protects the expected output from being quietly changed. It does not protect against an agent that edits the test code to remove the verification call, or one that changes the comparison function to always pass. Test code is code, and it needs its own guard.

Code owners on the test directory is the cheapest control. A required review from a maintainer on any change under the tests directory means the agent's edit to the verification path is visible to a person before merge. Combine that with the fail-closed verifier, and an agent that removes the verifier call produces a test that raises an error rather than passes.

Mutation testing is the complementary signal for the suite as a whole. It introduces small changes into the source and reports which ones the tests failed to catch, so a suite that passes because its assertions were weakened is visibly weaker than one whose assertions still pull their weight as CircleCI explains. Run it periodically on the files around golden outputs. If a mutation in the rounding code survives, the golden file is not doing its job, and re-signing is not the answer. Expect some equivalent mutants that no test can kill, which the Stryker documentation describes as a normal and accepted part of the score per the Stryker mutants guide.

One more limit: signing protects the expected output, not the input. If a test generates a quote from a fixture input and the agent edits the input so the output matches, the signature on the golden file is intact and the test still passes for the wrong reason. Sign the input fixtures the same way, or generate the comparison from a fixed input defined in the test code.

Failure the agent might attemptPlain golden fileSigned golden fileSigned plus code owners
Edit expected output to match codePassesSignature errorSignature error, review required
Edit expected output and signaturePassesSignature errorSignature error, review required
Delete the signature fileError, if verifier fails closedErrorError
Remove the verifier call from the testPassesPassesReview catches it
Edit the input fixturePassesPasses, unless inputs are signedReview depends on scope

No single row shows a complete answer, which is why the guards stack.

Deciding when this is worth the machinery

Signing adds a key to manage, a script to run and a review rule to enforce. For a throwaway prototype, that is too much. For a repository where an autonomous agent opens pull requests that a small team reviews, it is a reasonable cost, and the decision rule is about risk, not about preference.

Sign the golden files when all three of these hold: the expected outputs encode values that matter to the business, such as money, entitlements, access decisions or serialized contracts; an agent can write to the repository without a person reading every line it produces; and the team cannot guarantee that someone will notice a quiet one-line change to a fixture file in a large diff. If any one is missing, start with code owners alone and add signing when the situation changes.

If your team is putting an expert on the job of reviewing agent output and wants a reviewer with a defined role rather than a general-purpose model, Quanta Experts is built for that handoff: each expert reads your context, does the work and returns finished deliverables you can see the experts here. That pairs naturally with signing, because the signature check is the mechanical part and the review is the judgment part.

FAQ

FAQ

Can an agent just recompute the signature if it can read the key?

Yes, and that is why the key must live outside the agent's reach. If the key is in the repository, an environment file the agent can read, or a config the agent can edit, the signature is a consistency check and nothing more. Keep it in the CI secret store and on maintainer machines only, and make the verifier fail closed when it is absent.

What happens on a fork pull request where secrets are not available?

The comparison job fails with a clear error, which is the intended behavior. An outside contributor's change to a golden file arrives unsigned or signed with a key you do not recognize, and a maintainer with the key re-signs after review. This is friction, and it is the correct amount of friction for a change to expected output.

Do I need to sign binary golden files like screenshots too?

The same HMAC approach works on any bytes, so a rendered image signs the same way a JSON file does. The important detail is to sign exactly what the test compares: if the comparison tolerates small pixel differences, sign a stable representation, such as a hash of the canonicalized image, rather than raw bytes that vary by renderer. Note that golden image tests have their own cross-platform stability problems, which is why some teams disable platform-specific goldens in CI as this Flutter golden testing guide describes.

How do I update a golden file when the behavior change is correct?

Run the signer by hand after verifying the new output against a spec or a hand-written example, commit the golden file and the signature together, and put the reason in the pull request description. The review rule then requires a code owner to approve the pair as one deliberate change, which is the point: the update is allowed, but it cannot happen by accident.

Does this replace mutation testing or code review?

No, it covers one specific vector. Signing stops silent edits to expected outputs, mutation testing tells you whether the assertions still catch defects as CircleCI explains, and code review on the test directory catches changes to the verification path itself. Use them together when agents write to the repository, and skip signing when humans are the only ones touching the golden files.

Sources

Sources

  1. terminology - What are "golden files"?
  2. Golden Files, Why you should use them | by Ibrahim Jarif
  3. 5 Ways to Check Agent-Generated Code for Regressions
  4. Make One Coding Agent Write the Failing Tests and ...
  5. hmac, Keyed-Hashing for Message Authentication
  6. Some out-of-the-box hooks for pre-commit
  7. Designing Autonomous Agent Loops: Architecture Beyond Prompts - mign
  8. How to Test AI Agents - Medium
  9. How AI Agent Verification Prevents Production Bugs Before ...
  10. What is mutation testing? - CircleCI
  11. Equivalent mutants | Stryker Mutator
  12. Golden File Testing
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles