DEV Community

Emery Yang
Emery Yang

Posted on

Fail Extra Paths: A 90-Minute Agent Diff Spike

Agents treat the whole repo as in-scope. Extra paths then land inside the review diff. A 90-minute allowlist gate produces ship-or-kill evidence. Ship the gate when extra paths appear in git. Kill the agent loop if the gate only nags.

Hypothesis, one line

Cheap agent edits make surprise files cheap too. The spike tests a single claim. An unconstrained coding agent will touch paths outside a frozen allowlist.

Ship criterion: a local git gate fails those runs. Kill criterion: no extra paths, or the gate is noise. This is not a model ranking exercise. It is checkout discipline. Architecture talk can wait.

Why extra paths matter now

Agent write-ups still reward bigger loops. Cheap code also cheapens side edits. Refactors leak into configs. Lockfiles move without a request. Formatter noise hides the real patch.

Reviewers read the task, not the tree. Tests can pass on the wrong files. Debt arrives as a green CI. A path allowlist is a small contract. It is cheaper than another prompt.

Recent agent threads keep circling assumption errors. The filesystem is the assumption that ships. If the agent can see every package, it will “help” every package. Bound the tree before you bound the model.

Timer rules

Ninety minutes. One repo. One task. One gate.

In scope

  • Freeze HEAD and a path allowlist
  • Give the agent one concrete edit
  • Run a post-diff path gate
  • Write a four-line ship-or-kill note

Out of scope

  • Prompt rewrites after the first fail
  • New agent frameworks
  • Latency charts
  • Production rollout

Stop at minute 90. Incomplete evidence is still evidence. A missing kill note is a fail.

Minute 0–10: freeze the task

Pick a change that should touch two paths. Example: add a client timeout. Expected files stay tiny on purpose.

# allowlist.txt
src/http/client.ts
src/http/client.test.ts
Enter fullscreen mode Exit fullscreen mode

Write the allowlist before the agent starts. Do not edit it after the diff. That late edit would destroy the spike.

git rev-parse HEAD > /tmp/spike-head
test -z "$(git status --porcelain)" || {
  echo "dirty tree; abort spike" >&2
  exit 1
}
cp allowlist.txt /tmp/spike-allowlist
Enter fullscreen mode Exit fullscreen mode

The tree must be clean. Dirty files poison path evidence. Untracked junk looks like agent drift.

State the task in one paragraph. No architecture tour. No “also clean types”.

Example task card (proposal, unexecuted):

Add a 2500ms timeout to src/http/client.ts.
Cover it in src/http/client.test.ts.
Do not format other packages.
Do not touch lockfiles.
Enter fullscreen mode Exit fullscreen mode

Swap those paths for your tree. Keep the card shorter than the allowlist.

Minute 10–30: build the gate

The gate is the artifact. Keep it local. Keep it boring. The script below is meant to run after the agent stops.

#!/usr/bin/env bash
# fail-extra-paths.sh
# Fail when git changes walk off allowlist.txt
set -euo pipefail

ALLOWLIST="${1:-allowlist.txt}"
BASE_FILE="${2:-/tmp/spike-head}"
MAX_LINES="${MAX_LINES:-200}"

if [[ ! -f "$BASE_FILE" ]]; then
  echo "missing base sha file: $BASE_FILE" >&2
  exit 2
fi

if [[ ! -f "$ALLOWLIST" ]]; then
  echo "missing allowlist: $ALLOWLIST" >&2
  exit 2
fi

BASE="$(tr -d '[:space:]' < "$BASE_FILE")"

mapfile -t allowed < <(grep -E -v '^(#|$)' "$ALLOWLIST")
if [[ ${#allowed[@]} -eq 0 ]]; then
  echo "allowlist is empty" >&2
  exit 2
fi

mapfile -t changed < <({
  git diff --name-only "$BASE"
  git ls-files --others --exclude-standard
} | awk 'NF && !seen[$0]++')

if [[ ${#changed[@]} -eq 0 ]]; then
  echo "no changes against $BASE"
  exit 3
fi

fail=0
extra=()
missing=()

for path in "${changed[@]}"; do
  ok=0
  for a in "${allowed[@]}"; do
    if [[ "$path" == "$a" ]]; then
      ok=1
      break
    fi
  done
  if [[ "$ok" -eq 0 ]]; then
    extra+=("$path")
    fail=1
  fi
done

if [[ "${REQUIRE_ALL:-0}" == "1" ]]; then
  for a in "${allowed[@]}"; do
    found=0
    for path in "${changed[@]}"; do
      if [[ "$path" == "$a" ]]; then
        found=1
        break
      fi
    done
    if [[ "$found" -eq 0 ]]; then
      missing+=("$a")
      fail=1
    fi
  done
fi

lines="$(git diff --numstat "$BASE" | awk '{i+=$1; d+=$2} END {print i+d+0}')"
if [[ "$lines" -gt "$MAX_LINES" ]]; then
  echo "line budget exceeded: $lines > $MAX_LINES"
  fail=1
fi

echo "base: $BASE"
echo "line churn: $lines (max $MAX_LINES)"
echo "changed paths:"
printf '  %s\n' "${changed[@]}"

if [[ ${#extra[@]} -gt 0 ]]; then
  echo "extra paths (fail):"
  printf '  %s\n' "${extra[@]}"
fi

if [[ ${#missing[@]} -gt 0 ]]; then
  echo "missing allowlist paths (fail):"
  printf '  %s\n' "${missing[@]}"
fi

if [[ "$fail" -ne 0 ]]; then
  echo "SPIKE FAIL: extra paths, missing paths, or line budget"
  exit 1
fi

echo "SPIKE PASS: diff stays on allowlist"
exit 0
Enter fullscreen mode Exit fullscreen mode

Make it executable once. Do not pretty-print it later.

chmod +x fail-extra-paths.sh
Enter fullscreen mode Exit fullscreen mode

Optional line budget sits in MAX_LINES. Default is 200. Tune only before the agent runs. Tuning after the diff is cheating.

Minute 30–70: run one agent pass

Use whatever coding agent you already trust. One pass. No retry storm. Paste the task card. Point it at the two files. Then stop typing.

A scratch host is enough for this loop. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode provides free model access and a free server option. Treat that pair as a disposable runner for the spike. Do not treat it as production capacity. The gate does not depend on that host. Any local agent checkout works the same.

If the agent asks to “clean adjacent files”, refuse. Adjacent files are the failure mode. If it rewrites the allowlist, that is also a fail. Freeze the contract outside the repo if needed.

# keep the contract outside the agent sandbox if the tree is writable
cp allowlist.txt /tmp/spike-allowlist
Enter fullscreen mode Exit fullscreen mode

Do not watch tokens during this window. Token burn is a different spike. This spike only counts paths and line churn.

Minute 70–85: read the evidence

Run the gate against frozen HEAD. Do not rebase first. Do not stash agent junk.

./fail-extra-paths.sh /tmp/spike-allowlist /tmp/spike-head
echo "exit: $?"
git diff --stat "$(cat /tmp/spike-head)"
Enter fullscreen mode Exit fullscreen mode

Classify every extra path. Use the table. Do not argue with the model.

Extra path kind Likely cause Spike reading
Formatter / import sort in other packages Repo-wide save hook Gate stays; hook is in scope
Lockfile without a requested dep Agent “helpfully” upgraded Fail the run
README or changelog Narrative padding Fail the run
Snapshots you did not name Test tooling drift Fail, then tighten the card
The two allowlisted files only Bound behavior Pass
Zero files Agent stalled Exit 3; kill or retry once

REQUIRE_ALL=1 catches the stall case. It also fails partial edits. Use it when both files are mandatory.

REQUIRE_ALL=1 MAX_LINES=120 ./fail-extra-paths.sh /tmp/spike-allowlist /tmp/spike-head
Enter fullscreen mode Exit fullscreen mode

Record the exit code. Record extra paths verbatim. Screenshots are optional. The path list is not.

Minute 85–90: ship or kill

Write four lines. No blog voice. No vendor recap.

HYPOTHESIS: unconstrained agent will leave extra paths
RESULT: PASS gate / FAIL gate / NO CHANGE
EVIDENCE: <exit code>, <extra paths or none>, <line churn>
DECISION: SHIP path gate into the agent wrapper / KILL this workflow
Enter fullscreen mode Exit fullscreen mode

Ship if extra paths appeared even once. The gate earned its keep. Kill if the agent stayed inside the list and the gate added only friction. Kill if the task was too vague to judge. Vague tasks are not model failures.

Do not “improve the prompt” after the clock. That is a second spike. Mix those and the evidence collapses.

Wrapper sketch, not a platform

If you ship, wrap the agent command. Keep stdin as the task card. Keep stdout as logs. Keep the gate as the process exit.

#!/usr/bin/env bash
set -euo pipefail
# Proposed wrapper. Not production. Not executed here.

allowlist="${ALLOWLIST:-allowlist.txt}"
git rev-parse HEAD > /tmp/spike-head
test -z "$(git status --porcelain)"

# replace with your agent invocation
"$@"

./fail-extra-paths.sh "$allowlist" /tmp/spike-head
Enter fullscreen mode Exit fullscreen mode

The wrapper should fail closed. A crashed agent still leaves a diff. Run the gate on EXIT as well.

trap './fail-extra-paths.sh "$allowlist" /tmp/spike-head' EXIT
Enter fullscreen mode Exit fullscreen mode

Trap plus set -e needs care. Test the trap on a dummy extra file. Do that inside the 90 minutes if time remains. Skip it if the clock is gone.

Limitations

The allowlist is exact paths, not globs. Generated folders will false-fail. Rename-heavy refactors will false-fail. Binary assets will blow MAX_LINES in odd ways. git diff --numstat reports - for binaries. The awk sum then undercounts. Add a binary check if your tree has them.

git diff --numstat "$(cat /tmp/spike-head)" | awk '$1 == "-" || $2 == "-" { print "binary:", $3; bad=1 } END { exit bad+0 }'
Enter fullscreen mode Exit fullscreen mode

The spike does not prove prompt quality. It does not prove model quality. It proves whether the checkout is a contract. Multi-agent trees need one allowlist per agent. A shared list hides who drifted.

Free model access can be slower than a paid loop. Speed is out of scope. Do not convert this spike into a latency bake-off. Do not invent a winner from one run.

Who should not use this

Skip this spike if the work is a genuine repo-wide migration. Skip it if generated code is the product. Skip it if your VCS is not git. Skip it if reviewers already enforce file-level CODEOWNERS with no exceptions. Skip it if you cannot freeze HEAD for ninety minutes.

Do not use the gate as a people metric. Extra paths are a process smell. They are not a developer score. Do not paste vendor names into the kill note. The note is for the wrapper, not marketing.

What this spike refuses to claim

No quota numbers appear here. No hardware claims appear here. No permanent free-tier promise appears here. Availability can change. Re-read the product docs before you schedule a team ritual. The method survives that change. The host might not.

If you run the same spike on a free server, keep the allowlist and the kill note. That pair is the artifact. The rest is noise.

Top comments (0)