You unzip two take-homes on a Sunday night. Both folders look staff-level. Tests are green. The README has the calm voice of someone who has already shipped the same service twice.
Then you see the part no rubric mentions. One transcript is a long paid-model session. The other candidate stopped when a trial key died. You did not mean to hire a credit card. You hired one anyway.
That is the hole in a lot of “AI-friendly” interviews in 2026. The task looks technical. The hidden filter is billing. If the only way to finish your take-home is a personal API key, you are scoring subscription access, not judgment.
So stop sending a repo tour. Send a prompt packet: the task, the contract you will grade, a sample path, and the failure modes that still fail when the code is pretty. Make the packet runnable on a free coding-agent runtime so every candidate starts from the same side of the paywall.
The scene you are actually testing
You are not testing whether someone can vibe a CRUD app. You are testing whether they can keep an assistant inside a written contract when the clock is real and the machine is ordinary.
Picture a candidate on a laptop that is also playing music. They paste your prompt into an agent. The agent offers to “just rewrite the module.” A weak candidate says yes. A stronger one pastes the contract back into the thread and makes the agent argue with the file, not with a marketing sentence.
That difference does not show up in a polished demo. It shows up in a packet you wrote before anyone cloned the repo.
The prompt you actually send
Here is a proposed take-home, not a war story. You can drop this file in the zip as PROMPT.md and refuse to answer clarifying questions that the file already covers.
# Take-home: contract_check
You will implement a small CLI named `contract_check`.
Time box: one sitting. Do not build a platform.
Runtime: you may use an AI coding assistant. You may not use a paid API key
as a requirement of the solution. If your usual tool needs billing, switch
to a free model/runtime or write the code yourself.
## Behavior
Read a JSON contract and a unified diff on stdin. Print a JSON report on
stdout. Exit 0 only when the diff honors the contract.
Contract fields:
- max_files: int
- max_diff_lines: int
- forbidden_globs: list of fnmatch patterns (match against paths in the diff)
- required_test_globs: list of fnmatch patterns; at least one added/modified
file must match
Report fields:
- ok: bool
- files_touched: int
- diff_lines: int
- forbidden_hits: list of paths
- missing_tests: bool
- notes: list of short strings
## Rules
1. Do not fetch the network during grading.
2. Do not read files outside the diff and the contract.
3. Commit TOOLS.md: which assistant, which runtime, and what you forbade it to do.
4. If you change the prompt, say so in NOTES.md. Silent prompt edits fail.
Notice what is missing. There is no “impress us.” There is no hidden production repo. The prompt is the product. If a candidate cannot finish it without buying tokens, the prompt is the thing you should rewrite, not the person.
If you need a shared place for people who do not already rent a coding agent, MonkeyCode’s free model access and free server option is one way to keep the take-home from becoming a wallet test. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Use it as a runtime, not as a brand name in the rubric. The rubric still has to fail pretty code.
A contract file, not a vibe
Ship the contract next to the prompt. Candidates should not have to guess what “small” means.
{
"max_files": 4,
"max_diff_lines": 120,
"forbidden_globs": [
"secrets/*",
"**/.env",
"**/id_rsa*"
],
"required_test_globs": [
"tests/test_*.py",
"*_test.py"
]
}
You can narrate this in the interview later. Four files is a fence, not a style opinion. One hundred and twenty diff lines is a fence. Forbidden globs are a fence. Required tests are a fence. Fences are kinder than “use your judgment,” because judgment without a fence is just whoever talks first.
Sample solution (labeled, not claimed production)
This is a sample path a candidate might commit. It is intentionally boring. Boring is the point. If your assistant cannot stay boring, that is data.
#!/usr/bin/env python3
"""contract_check.py — sample take-home solution, not a library."""
from __future__ import annotations
import fnmatch
import json
import re
import sys
from pathlib import Path
FILE_RE = re.compile(r"^(?:--- |\+\+\+ )([ab]/)?(.+)$")
def parse_diff(text: str) -> tuple[list[str], int]:
files: list[str] = []
diff_lines = 0
for raw in text.splitlines():
if raw.startswith("--- ") or raw.startswith("+++ "):
m = FILE_RE.match(raw)
if not m:
continue
path = m.group(2).strip()
if path == "/dev/null":
continue
if path not in files:
files.append(path)
continue
if raw.startswith("+++") or raw.startswith("---"):
continue
if raw.startswith("+") or raw.startswith("-"):
if raw.startswith("+++") or raw.startswith("---"):
continue
diff_lines += 1
return files, diff_lines
def grade(contract: dict, diff_text: str) -> dict:
files, diff_lines = parse_diff(diff_text)
forbidden = []
for path in files:
for pat in contract["forbidden_globs"]:
if fnmatch.fnmatch(path, pat):
forbidden.append(path)
break
missing_tests = not any(
fnmatch.fnmatch(path, pat)
for path in files
for pat in contract["required_test_globs"]
)
notes = []
if len(files) > contract["max_files"]:
notes.append("too many files")
if diff_lines > contract["max_diff_lines"]:
notes.append("diff too large")
if forbidden:
notes.append("forbidden path")
if missing_tests:
notes.append("no test file in diff")
ok = not notes
return {
"ok": ok,
"files_touched": len(files),
"diff_lines": diff_lines,
"forbidden_hits": forbidden,
"missing_tests": missing_tests,
"notes": notes,
}
def main() -> int:
if "--contract" not in sys.argv:
print("usage: contract_check.py --contract contract.json < patch.diff", file=sys.stderr)
return 2
path = Path(sys.argv[sys.argv.index("--contract") + 1])
contract = json.loads(path.read_text())
report = grade(contract, sys.stdin.read())
json.dump(report, sys.stdout, indent=2)
sys.stdout.write("\n")
return 0 if report["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())
A candidate should be able to run it without a story:
python3 contract_check.py --contract contract.json < fixtures/good.patch
echo $?
python3 contract_check.py --contract contract.json < fixtures/touches_env.patch
echo $?
If those two commands do not exist in the zip, you are grading conversation again. Conversation is a demo. Exit codes are a receipt.
The rubric lives next to the prompt
Do not keep the score in a private doc the candidate never sees. That is how you accidentally grade confidence. Put a machine-readable rubric in the zip and use it as the only scoring sheet.
# rubric.yaml — proposed scoring, 10 points
contract_honored: 4 # exit 1 on forbidden path / oversized diff / missing test
no_network: 2 # grader runs with network blocked
tools_md: 2 # assistant, runtime, and refusals written down
notes_md: 1 # silent prompt edits are a zero on this row
boring_code: 1 # extra frameworks score zero, not extra credit
You can still talk in the debrief. Talk after the numbers. If a candidate used a free runtime and left TOOLS.md looking like a crime scene — “I forbade file deletes, I forbade .env, I reran the failing fixture” — that is the interview. If they used a paid model and left TOOLS.md empty, you learned something cheaper than a loop.
A tiny lock for the “no network” row keeps you honest:
# proposed local gate; not a production sandbox
python3 - <<'PY'
import socket, sys
socket.socket = lambda *a, **k: (_ for _ in ()).throw(RuntimeError("network blocked"))
sys.argv = ["contract_check.py", "--contract", "contract.json"]
import runpy
runpy.run_path("contract_check.py", run_name="__main__")
PY
If their CLI imports requests “just in case,” the gate throws. Pretty code that phones home fails the same way ugly code does. That is the whole job of a take-home in an agent year.
Failure modes you write before you meet them
Write the failures as fixtures, not as vibes in a hiring doc. A fixture is a short story the agent cannot charm.
The first failure is the secret path. The diff looks like a test fix. Buried in it is --- a/.env and a single plus line. Candidates who grade “does it compile?” will ship it. Candidates who grade the contract will not. Name the file fixtures/touches_env.patch and keep it in the zip.
The second failure is the generous rewrite. The assistant replaces four modules because “the structure was unclear.” files_touched walks past max_files while every function has a docstring. This is the 2026 version of a candidate who refactors your interview to avoid the bug. If your rubric cannot fail a beautiful diff, you do not have a rubric.
The third failure is the missing test that is not missing if you squint. They add test.md with a paragraph about coverage. required_test_globs does not match. Argue in the debrief if you want. Do not argue during scoring.
The fourth failure is the silent prompt edit. They delete the no-network rule because the agent kept apologizing. NOTES.md is empty. This is not creativity. This is changing the exam while you take it. Score that row zero even if contract_check is perfect.
The fifth failure is the paid-key dependency that sneaks back in. A wrapper that will not start without OPENAI_API_KEY is not a solution to this packet. It is a polite way of saying other people cannot run the homework. Fail it. You asked for a checker, not a vendor SDK.
What this packet is not
This is not a staff-plus architecture interview. If you need someone to unstick a multi-region outage, do not pretend a 120-line diff will tell you. It will not.
This is also not a license to offload proprietary product code onto a shared server. If your legal team does not want candidate patches on an external runtime, do not use one. Run the same prompt on a laptop you imaged yourself. The prompt still works. The fairness argument still works. The only thing that changes is who owns the disk.
Do not use this approach for roles where AI assistance is forbidden. A prompt that says “you may use an assistant” is a different exam from “you may not.” Mixing them is how you get a week of disputes.
And do not treat a free runtime as a quality oracle. Free model access can be enough to finish a fenced CLI and still be the wrong tool for a gnarly production bug. The point is equality of entry, not a ranking of vendors. If the packet only passes when the candidate buys a better model, you wrote a shopping list.
How you run the debrief
Open TOOLS.md first. Ask what they forbade the agent to do. Then open the failing fixture they did not expect. Ask whether they would loosen max_diff_lines or keep it. You are listening for whether they can defend a fence.
Keep the paid-transcript flex out of the room. If two people finished on a free server and one person finished on a personal key, score the contract, not the invoice. The industry is already loud about whether AI codes “better than most developers.” Your take-home does not need to join that argument. It needs to tell you if this person can keep a tool inside a file they agreed to.
If you want every candidate on the same side of the paywall, run the packet on a free model and a free server once yourself before you send it. When the prompt still holds on that runtime, you are hiring a person. When it only holds on a billed session, you are hiring a plan.
Top comments (0)