{"slug": "your-ai-coding-agent-doesn-t-need-a-better-prompt-it-needs-a-harness", "title": "Your AI Coding Agent Doesn't Need a Better Prompt. It Needs a Harness.", "summary": "A developer outlined a \"harness engineering\" approach for AI coding agents, arguing that reliable agent output depends less on prompt tuning than on the surrounding engineering environment. The walkthrough builds a lightweight Node.js harness — an AGENTS.md context file, a guardrails script, and a test suite run via npm run check — that gives an existing coding agent like Codex or Claude Code verifiable pass/fail feedback on its changes, with no paid API or npm dependencies.", "body_md": "Your AI coding agent just announced:\n\n✅ Done! Everything works. All tests passed.\n\nExcellent. Small problem: **your project doesn't have any tests.**\n\nSo you write a better prompt. “Double check your work.” “Follow best practices.” “Act like a senior engineer.” You might as well add “pretty please” and offer it a cookie.\n\nThe agent says “Absolutely!” and breaks something else.\n\nHere's my unpopular opinion: **we spend too much time trying to write perfect prompts for agents working inside terrible engineering environments.**\n\nA better prompt can help. But a prompt can't replace tests, project context, architectural boundaries, or an actual feedback loop.\n\nThat's the idea behind **harness engineering**: design the environment around a coding agent so its work can be guided, checked, and corrected.\n\nAnd yes, we're building one. **In about 15 minutes, with Node.js, no paid API, and no npm dependencies.** The harness works with an existing coding agent that can read files and run terminal commands; we're *not* building another agent framework.\n\n**What you'll build:**\n\n`AGENTS.md` to explain the repository and its rules.`npm run check`) that provides real feedback.\nA prompt says: **“Don't break existing functionality.”**\n\nA harness says: **“Here are the regression tests. Run them.”**\n\nA prompt says: “Stick to the architecture.”\n\nA harness says: “Read the architecture docs, and here's a check that rejects forbidden imports.”\n\nA prompt says: “Make sure it works.”\n\nA harness says: **“Show me the exit code.”**\n\nNot the same thing. Also, not enemies: a good prompt tells the agent *what you want*; a harness gives it an environment in which progress is *observable and verifiable*.\n\n```\nTask + constraints\n       |\n       v\n  Coding agent  <----- failed check output\n       |                    ^\n       v                    |\n Code changes --> npm run check\n                       |\n                  pass / fail\n                       |\n                 Human review\n```\n\nLet's make that real before the buzzword develops its own certification program.\n\n**Prerequisites:** Node.js 22+, Git, and a coding agent with access to your local repository (Codex, Claude Code, or any equivalent). You can complete the entire exercise manually too.\n\n```\nmkdir agent-harness-lab && cd agent-harness-lab\nnpm init -y\nmkdir -p src test scripts docs .github/workflows\n```\n\nReplace `package.json` with:\n\n```\n{\n  \"name\": \"agent-harness-lab\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"engines\": { \"node\": \">=22\" },\n  \"scripts\": {\n    \"test\": \"node --test\",\n    \"guardrails\": \"node scripts/guardrails.js\",\n    \"check\": \"npm run guardrails && npm test\"\n  }\n}\n```\n\nCreate `src/pricing.js`:\n\n``` js\nexport function calculateTotal(items, discountPercent = 0) {\n  const subtotal = items.reduce(\n    (sum, item) => sum + item.price * item.quantity,\n    0\n  );\n\n  return Math.round(\n    (subtotal * (1 - discountPercent / 100)) * 100\n  ) / 100;\n}\n```\n\nLooks fine, right? So does a chair with three legs, until someone sits down.\n\nThis function accepts negative quantities, `NaN` prices, and 120% discounts. Let's make those failures impossible to ignore.\n\nCreate `test/pricing.test.js`:\n\n``` python\nimport test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { calculateTotal } from '../src/pricing.js';\n\ntest('calculates normal totals', () => {\n  assert.equal(calculateTotal([{ price: 12.5, quantity: 2 }]), 25);\n});\n\ntest('applies a valid discount', () => {\n  assert.equal(calculateTotal([{ price: 100, quantity: 1 }], 15), 85);\n});\n\ntest('rejects negative quantities', () => {\n  assert.throws(\n    () => calculateTotal([{ price: 10, quantity: -1 }]),\n    RangeError\n  );\n});\n\ntest('rejects discounts above 100%', () => {\n  assert.throws(\n    () => calculateTotal([{ price: 10, quantity: 1 }], 120),\n    RangeError\n  );\n});\n\ntest('rejects non-finite prices', () => {\n  assert.throws(\n    () => calculateTotal([{ price: NaN, quantity: 1 }]),\n    TypeError\n  );\n});\n\ntest('does not mutate the cart', () => {\n  const items = [{ price: 10, quantity: 2 }];\n  const before = structuredClone(items);\n  calculateTotal(items);\n  assert.deepEqual(items, before);\n});\n```\n\nRun:\n\n```\nnpm test\n```\n\n**Expected result: 3 tests pass, 3 fail.** That's good news. We've converted a vague request into a measurable target.\n\nAnd yes, this example uses JavaScript floating point numbers to stay focused on agent workflows. For real money, use integer minor units or a decimal strategy with documented rounding rules.\n\nCreate `AGENTS.md` at the repository root:\n\n```\n# Project instructions\n\n## Context\n- Node.js 22+, ESM, no runtime dependencies.\n- Pricing logic: src/pricing.js\n- Tests: test/pricing.test.js\n- Architecture notes: docs/architecture.md\n\n## Rules\n- Preserve calculateTotal(items, discountPercent = 0).\n- Do not change tests or guardrails to make a check pass.\n- Do not add packages without approval.\n- Do not access secrets, deploy, publish, or push changes.\n- Keep changes scoped to the task.\n\n## Verification\n- Run: npm run check\n- Report the actual result, including failures.\n- Review git diff before claiming completion.\n\n## Definition of done\n- Requested behavior works.\n- npm run check passes.\n- No unrelated changes.\n- Remaining risks are listed for human review.\n```\n\nCreate `docs/architecture.md`:\n\n```\n# Architecture\n\nThe public API is calculateTotal(items, discountPercent = 0).\nProduction code belongs in src/; tests belong in test/.\nThe pricing function must be pure and must not perform I/O.\nInvalid inputs must throw, not silently produce nonsense totals.\n```\n\nOne subtle point: **Codex recognizes `AGENTS.md` as a repository instruction file**, but agent products differ. For other tools, verify their convention or explicitly tell the agent to read the file. A markdown filename isn't telepathy.\n\nAnd this file is guidance, **not a security boundary**. If an agent has permission to delete production resources, writing “please don't” in Markdown won't revoke that permission.\n\nCreate `scripts/guardrails.js`:\n\n``` js\nimport { readFileSync, readdirSync } from 'node:fs';\n\nconst pkg = JSON.parse(readFileSync('package.json', 'utf8'));\n\nfor (const field of ['dependencies', 'optionalDependencies', 'peerDependencies']) {\n  if (Object.keys(pkg[field] ?? {}).length) {\n    throw new Error(`${field} are not allowed in this lab`);\n  }\n}\n\nfor (const file of readdirSync('src').filter((f) => f.endsWith('.js'))) {\n  const source = readFileSync(`src/${file}`, 'utf8');\n\n  // Narrow teaching example: no imports in src/*.js.\n  if (/\\b(?:import|require)\\s*(?:\\(|[{\\'\"*])/m.test(source)) {\n    throw new Error(`Unexpected import in src/${file}`);\n  }\n}\n\nconsole.log('Architectural guardrails passed.');\n```\n\nThis checks two *specific* constraints: no declared runtime dependencies and no obvious imports in top level `src/*.js` files. It is **not** a general security scanner, and regex is not a robust JavaScript parser. For serious projects, use AST aware lint rules and dependency policies.\n\nNow run:\n\n```\nnpm run check\n```\n\nThe guardrail passes; the three behavior tests still fail. That's the harness doing its job, not an agent having a bad day.\n\nOpen the repository with your coding agent and paste:\n\n```\nRead AGENTS.md and docs/architecture.md before editing.\n\nFix src/pricing.js so all existing pricing tests pass.\n\nConstraints:\n- Preserve calculateTotal(items, discountPercent = 0).\n- Negative quantities: RangeError.\n- Discounts outside 0..100: RangeError.\n- Non finite prices: TypeError.\n- Keep the function pure.\n- Change only src/pricing.js.\n- Do not install dependencies or weaken tests.\n\nRun npm run check and inspect its actual output.\nIf it fails, fix the cause and rerun, up to 3 attempts.\nDo not claim success unless the command succeeds.\n\nReport files changed, check results, and remaining risks.\n```\n\nNotice what changed: the prompt is still important, but it now points to an **independent contract**.\n\nA possible passing implementation is:\n\n```\nexport function calculateTotal(items, discountPercent = 0) {\n  if (!Array.isArray(items)) {\n    throw new TypeError('items must be an array');\n  }\n\n  if (!Number.isFinite(discountPercent) ||\n      discountPercent < 0 || discountPercent > 100) {\n    throw new RangeError('discountPercent must be between 0 and 100');\n  }\n\n  const subtotal = items.reduce((sum, item) => {\n    if (!item || typeof item !== 'object' || !Number.isFinite(item.price)) {\n      throw new TypeError('price must be finite');\n    }\n    if (!Number.isInteger(item.quantity) || item.quantity < 0) {\n      throw new RangeError('quantity must be nonnegative');\n    }\n    if (item.price < 0) {\n      throw new RangeError('price must be nonnegative');\n    }\n    return sum + item.price * item.quantity;\n  }, 0);\n\n  return Math.round(subtotal * (1 - discountPercent / 100) * 100) / 100;\n}\n```\n\nCheck it yourself:\n\n```\nnpm run check\ngit diff --check\ngit diff\n```\n\n**Expected after applying that fix: all 6 tests pass.** Passing tests don't prove every possible behavior is correct, and `git diff` still needs human review. But at least we're discussing evidence instead of vibes.\n\nThe useful loop is:\n\n``` php\nEdit -> Run checks -> Read real failure -> Fix -> Run again\n                                     |\n                            Stop after 3 attempts\n                            and request review\n```\n\nAn agent that retries forever isn't autonomous. It's a subscription renewal strategy.\n\nCreate `.github/workflows/check.yml`:\n\n```\nname: Check\non: [push, pull_request]\n\npermissions:\n  contents: read\n\njobs:\n  verify:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: '22'\n      - run: npm run check\n      - run: git diff --check\n```\n\nOpen a pull request. Now CI runs the same checks as your local agent.\n\nFor production use, pin third party GitHub Actions to vetted commit SHAs, enforce required checks and reviews, and protect tests and guardrails (for example with CODEOWNERS and branch rules). **An agent can make its own evaluation look green by weakening the evaluator if you let it edit everything.**\n\nAlso: run agent executed code in a suitably restricted environment. Tests don't replace sandboxing, permissions, or approval gates for destructive actions.\n\nBefore, your quality strategy was:\n\n“Please be careful.”\n\nNow you have:\n\n| Problem | Harness response | \n|---|---|\n| Agent invents test results | Run `npm run check` and read the actual exit code | \n| Agent forgets project conventions | `AGENTS.md` + architecture notes | \n| Agent introduces forbidden imports | Architectural guardrail fails | \n| Agent breaks behavior | Regression tests fail | \n| Agent declares victory early | CI and human review remain separate gates | \n\nYou can also preserve longer tasks using a short `docs/HANDOFF.md` with the objective, files changed, real verification results, blockers, and next action. Don't paste an entire chat transcript and call it documentation.\n\nTake five comparable tasks. Use the same model, starting repository state, allowed attempts, and time budget. Compare **prompt-only** versus **prompt + harness**. Track:\n\nAvoid pretending a single toy example proves a universal performance gain. The point of this lab is to demonstrate the mechanism, not manufacture a benchmark.\n\n**Prompt engineering isn't dead. But “write a better prompt” is not a complete software engineering strategy.**\n\nIf you give an agent no tests, no context, no guardrails, and unlimited permissions, it can fail very efficiently. Congratulations: you've automated chaos.\n\nA harness makes failure visible and gives the agent a structured chance to recover. That doesn't guarantee correctness. It gives *you* a way to evaluate it.\n\nThe goal isn't an agent that confidently announces:\n\nEverything is production ready! 🚀\n\nIt's a workflow that can prove:\n\n```\n✓ Architecture checks passed\n✓ 6 behavior tests passed\n✓ Changes reviewed before merging\n→ Ready for human approval\n```\n\n**What was the last thing your AI coding agent confidently broke?** And more importantly: what single automated check would have caught it?\n\nTell me in the comments. I collect these stories the way some people collect Pokémon.\n\n**Further reading:**", "url": "https://wpnews.pro/news/your-ai-coding-agent-doesn-t-need-a-better-prompt-it-needs-a-harness", "canonical_source": "https://dev.to/holasoymalva/your-ai-coding-agent-doesnt-need-a-better-prompt-it-needs-a-harness-9ej", "published_at": "2026-11-11 23:38:51+00:00", "updated_at": "2026-11-11 23:52:20.144339+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "artificial-intelligence"], "entities": ["Node.js", "Codex", "Claude Code", "Git"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/your-ai-coding-agent-doesn-t-need-a-better-prompt-it-needs-a-harness", "markdown": "https://wpnews.pro/news/your-ai-coding-agent-doesn-t-need-a-better-prompt-it-needs-a-harness.md", "text": "https://wpnews.pro/news/your-ai-coding-agent-doesn-t-need-a-better-prompt-it-needs-a-harness.txt", "jsonld": "https://wpnews.pro/news/your-ai-coding-agent-doesn-t-need-a-better-prompt-it-needs-a-harness.jsonld"}}