DEV Community

Riley Lin
Riley Lin

Posted on

Rebuild the Context Every Turn

Most teams audit the single prompt they are about to send and never audit the transcript that carries it, which means the real leak is the session rather than the message. A long chat quietly accumulates stack traces, config dumps, and half-scrubbed logs from twenty turns ago, and nobody re-reads the scrollback before turn twenty-one. The fix is boring and cheap: treat context as a build artifact, rebuild it from a checked-in manifest on every turn, and throw the conversation history away.

Cheap tokens make the problem worse, not better

Free model access removes the only brake most people had on context size, which was the invoice. When every turn costs nothing, you keep one session alive all afternoon and keep appending \"just one more file\" until the window holds your entire working tree and a stray dotfile. MonkeyCode, an open-source coding tool, advertises free model access and a free server option; those are operator-supplied availability claims, and I am deliberately not restating token counts or machine specs here, because quotas move and stale numbers are worse than none. Check the project's own pages for current limits before you plan a budget around them.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The honest role of the free tier in this workflow is narrow: you need a place to run the build-and-scan step that is neither your laptop with a loaded .env nor your production runner. Everything below also works with any provider you already pay for, and most of it runs offline.

Four boundaries, only one of which people watch

Write the threat model down as boundaries, because \"be careful with secrets\" is a sentiment and not a control. The first boundary runs from your repository into the manifest, where one careless glob can sweep in .env and .npmrc without a reviewer noticing. The second runs from the manifest into the prompt, where line ranges drift as files change and a three-line slice silently becomes three hundred. The third runs from the prompt to the provider, and it is the only boundary most teams have any tooling for at all. The fourth runs from the response back into your shell or your CI job, and it is the one that actually executes something.

The risk order is roughly the reverse of the attention order: teams obsess over the third boundary and ignore the first and the fourth. That asymmetry is why a rebuilt context beats a growing transcript, since a build step has somewhere to put a check and a scrollback does not.

Build the context instead of extending it

Save this as context.manifest.json and keep it in review next to the code it feeds, so a new file path arrives as a diff rather than a habit.

{
  "tasks": {
    "review-auth": {
      "entries": [
        { "path": "src/auth/session.ts", "lines": [1, 120] },
        { "path": "docs/auth-contract.md" }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Here is a dependency-free builder for Node 20 and later, saved as build-context.mjs.

import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';

const PATTERNS = [
  [/AKIA[0-9A-Z]{16}/g, 'aws-access-key-id'],
  [/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, 'github-token'],
  [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, 'jwt'],
  [/[a-z]+:\/\/[^\s:@/]+:[^\s@/]+@/g, 'url-with-password'],
  [/-----BEGIN [A-Z ]*PRIVATE KEY-----/g, 'private-key']
];

function slice(text, lines) {
  if (!lines) return text;
  return text.split('\n').slice(lines[0] - 1, lines[1]).join('\n');
}

function scan(path, text) {
  return PATTERNS.flatMap(([re, label]) => {
    const hits = text.match(re);
    return hits ? [{ path, label, count: hits.length }] : [];
  });
}

const argv = process.argv.slice(2);
const taskId = argv.find((a) => !a.startsWith('--'));
const dryRun = argv.includes('--dry-run');

const manifest = JSON.parse(await readFile('context.manifest.json', 'utf8'));
const task = manifest.tasks[taskId];
if (!task) {
  console.error(`unknown task: ${taskId}`);
  process.exit(2);
}

const blocks = [];
const problems = [];
for (const entry of task.entries) {
  const text = slice(await readFile(entry.path, 'utf8'), entry.lines);
  problems.push(...scan(entry.path, text));
  const digest = createHash('sha256').update(text).digest('hex').slice(0, 12);
  blocks.push({ path: entry.path, digest, text });
}

if (problems.length > 0) {
  for (const p of problems) console.error(`BLOCK ${p.path}: ${p.label} x${p.count}`);
  console.error('refusing to build context; narrow the range or fix the source');
  process.exit(1);
}

const prompt = blocks.map((b) => `### ${b.path} (${b.digest})\n${b.text}`).join('\n\n');
console.error(`${taskId}: ${blocks.length} blocks, ${Buffer.byteLength(prompt, 'utf8')} bytes`);

if (dryRun) {
  for (const b of blocks) console.error(`${b.digest}  ${b.path}`);
  process.exit(0);
}
process.stdout.write(prompt);
Enter fullscreen mode Exit fullscreen mode

The digest is the part people skip and then regret, because it lets you compare two turns by hash instead of by eye. It also tells you immediately when a file you thought was frozen has moved underneath a range you trusted.

node build-context.mjs review-auth --dry-run
node build-context.mjs review-auth > /tmp/prompt.md && wc -c /tmp/prompt.md
Enter fullscreen mode Exit fullscreen mode

A test plan that fails on purpose

A scanner is decoration until it blocks something real, so prove the refusal with a fixture that plants one credential per pattern and a test that asserts the non-zero exit. Keep the fixtures in the repo and treat them as the specification for the loader.

// session.test.mjs — run with: node --test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';

function build(taskId, ...flags) {
  return execFileSync('node', ['build-context.mjs', taskId, ...flags], { encoding: 'utf8' });
}

test('a planted credential stops the build', () => {
  assert.throws(
    () => build('fixture-planted'),
    (err) => err.status === 1 && /BLOCK fixtures\/planted\.env/.test(String(err.stderr))
  );
});

test('the clean task stays under the byte budget', () => {
  const out = build('review-auth');
  assert.ok(Buffer.byteLength(out, 'utf8') < 40000);
  assert.ok(!out.includes('PRIVATE KEY'));
});
Enter fullscreen mode Exit fullscreen mode

Run node --test in CI before any request leaves the machine, and wire the build step in front of whichever model endpoint you actually use. The free server fits here naturally, since a gate that runs next to the code is harder to skip than a gate that runs next to your coffee.

What belongs in the manifest

Data class In the manifest? Rule Cost when wrong
Source under review Yes, with a line range Range must be explicit Silent drift into unrelated files
Docs and contracts Yes, whole file Pin by path, never by glob A glob drags in *.local.md
.env, .npmrc, key material Never Exclude at the loader Exposure with no diff to review
Production logs Only after field-level drop Drop columns before slicing User identifiers in someone's store
Model output No Quarantine, then re-enter as a fixture Injected instructions reach the shell

The last row is where careful teams still get caught, because a transcript becomes a delivery channel for instructions that originated in a file you never trusted. Rebuilding the context each turn breaks that channel, since the previous answer has to pass the same loader as everything else.

Limitations and who should skip this

Rebuilding every turn costs real effort, and on a genuinely tangled bug the manifest becomes a small research project of its own. Regex scanning is a tripwire and not a data-loss-prevention product: it will miss a secret format you never enumerated, and it will miss a credential split across two lines. Digests give you a diff handle and provenance, not confidentiality, and they will happily hash a file that should never have been read at all.

If your question genuinely needs the whole repository in view, this workflow will frustrate you, and a careful single long session plus a human review of the outgoing text may serve you better. If you operate under a compliance regime that mandates a reviewed DLP engine, use that engine and treat this builder as a pre-check rather than a replacement. And if you cannot pin your sources by path, fix that first, because every control below depends on knowing what the loader is allowed to open.

You do not need a budget to start. Run node build-context.mjs review-auth --dry-run, read the digest list, and delete the file the manifest forgot; one blocked build teaches more than a page of redaction policy, and the free tier is enough to run the loop end to end.

Top comments (0)