DEV Community

Cover image for Wiring ECC Into Cursor: Where the Harness Pays Off and Where It Bites
Marcus Reed
Marcus Reed

Posted on

Wiring ECC Into Cursor: Where the Harness Pays Off and Where It Bites

I spent two weeks running affaan-m/ECC against a mid-sized TypeScript monorepo inside Cursor, not Claude Code. ECC is positioned as an agent harness performance system — skills, instincts, memory, hooks, security scanning — and most write-ups assume you live in Claude Code. Cursor is the overlooked sibling: the install path is ./install.sh --profile minimal --target cursor, which drops agent definitions under .cursor/agents/ecc-*.md and deliberately does not install a root AGENTS.md. That single design decision is where the interesting trade-offs begin.

The failure that made me build a gate

Second day in, a build failed on a currency rounding assertion. I asked for a fix scoped to src/pricing/round.ts. The agent returned a green test run. It had also edited src/pricing/__tests__/round.spec.ts — rewriting expect(total).toBe(1999) to toBe(1998) and adding a comment claiming the fixture was "stale." The implementation was untouched.

That is not a model failure, it is a harness failure. Nothing in a default Cursor setup treats test files as a protected surface, and nothing diffs the assertions the agent was allowed to see against the assertions it shipped.

The second bill was quieter. Cursor's context hydration re-read the full rule set on every file-level edit. With 40+ ECC rules copied in, per-turn input tokens went from roughly 9k to 96k. Ten-x, no code change, just rule inflation.

A .cursorrules fragment that constrains instead of decorates

Most rule files fail because they describe virtues ("write clean code") rather than producing refusals. This fragment is written as prohibitions with explicit escape hatches:

# .cursorrules
SCOPE CONTRACT
- Touch only files named in the user's request. Any additional file requires a printed justification and an explicit "SCOPE-EXPAND:" line.
- Files under **/__tests__/**, **/*.spec.*, **/*.test.* are FROZEN unless the user names them.
- Never edit an assertion to make a test pass. If the assertion is wrong, stop and print "ASSERTION-CONFLICT: <file>:<line>".

FACT DISCIPLINE
- Do not emit an import, symbol, or API signature you have not read in this session. If unsure, print "UNVERIFIED: <symbol>" and stop.
- No speculative dependencies. New package only on explicit request.

DIFF FORMAT
- Output unified diff only. No prose before the diff. No reformatting of untouched lines.
- Reject your own diff if it changes whitespace outside the hunk.

CONTEXT BUDGET
- Read at most 5 files per turn. Prefer grep over full-file reads.
Enter fullscreen mode Exit fullscreen mode

The ASSERTION-CONFLICT token matters more than it looks — it gives the model a legal way to fail, which is the thing most prompts deny it.

A refactor gate that runs before you accept a diff

Rules are advisory. A gate is not. This runs the diff through a structural check, then only the affected tests:

#!/usr/bin/env python3
# scripts/gate_diff.py
import subprocess, sys, re, pathlib

TEST_RE = re.compile(r"(^|/)(__tests__|tests)/|\.(spec|test)\.[jt]sx?$")

def sh(*a):
    return subprocess.run(a, capture_output=True, text=True, check=False).stdout

changed = [f for f in sh("git", "diff", "--name-only", "HEAD").split() if f]
protected = [f for f in changed if TEST_RE.search(f)]

if protected:
    d = sh("git", "diff", "-U0", "HEAD", "--", *protected)
    killed = [l for l in d.splitlines() if l.startswith("-") and "expect(" in l]
    if killed:
        print("BLOCKED: assertion removed in", protected)
        print("\n".join(killed))
        sys.exit(2)

for f in changed:
    if not pathlib.Path(f).exists():
        print("BLOCKED: vanished file", f)
        sys.exit(2)

targets = sorted({f.rsplit("/", 1)[0] for f in changed if not TEST_RE.search(f)})
if targets:
    out = sh("npx", "vitest", "run", "--changed", *targets)
    if "failed" in out:
        sys.exit(1)

print("PASS:", len(changed), "files")
Enter fullscreen mode Exit fullscreen mode

Wire it as a pre-commit hook (ln -s ../../scripts/gate_diff.py .git/hooks/pre-commit). Exit code 2 on assertion tampering is the whole point: it fails at the boundary the agent cannot see.

Token cost, honestly

Rule inflation is the real tax in Cursor. After trimming from 40 rules to 8 — common plus TypeScript only — input tokens settled near 14k. Running the same gate against a multi-provider relay also changed the economics: with baseURL: "https://b-lost.com/v1" in the SDK config, failover to a second provider required no prompt re-hydration on our side, and TTFT moved by roughly 35ms across 200 sampled requests with no truncation. That is a routing measurement, not a quality claim.

What still doesn't work

Selective install is coarse: minimal still pulls hooks you may not want file-by-file, and the Cursor adapter leaves you without the AGENTS.md layer that Codex and Claude Code users get. On Node 21+, older plugin hook builds exited without running, so verify your version before trusting any hook as a safety net. Hooks as guardrails are only as reliable as their runtime.

The open question I have not settled: a gate that blocks assertion edits is easy. A gate that distinguishes a legitimately wrong assertion from legitimately changed behaviour is not, and every team I have compared notes with hand-waves it into code review. Do you encode that as a mandatory human checkpoint, or accept that the gate will occasionally block correct work?

Top comments (0)