Local work remains the default path for every repo job. A free remote hop is a privilege the wire must earn. Three checks decide whether bytes may leave disk at all.
Those checks cover leak risk, round-trip time, and link state. Fail any check and the job stays on the laptop.
A repo is a house with keys still in the bowl. A remote model is a truck on a public road. The truck can haul a heavy crate across town.
It should never roll away with the house keys. Small local edits rarely justify sending that public truck. Twenty tool hops turn a modest ping into seconds.
Local grep does not pay a handshake tax today. Offline is not an exotic outage in real work. Trains, planes, and locked labs still cut the socket.
A workflow that dies without a link is already broken. Build the door so missing sockets keep keys home.
The bouncer does not send bytes
The gate is a small Node script beside the repo. It reads a job path and a few flags. It returns local or remote and then exits.
Think of it as a bouncer at a club door. The bouncer does not sing on stage. The bouncer only checks IDs and pockets.
Check one scans text for secret assignment shapes. Check two samples one round trip on the wire. Check three honors an explicit offline flag from the operator.
The scanner is deliberately dumb on purpose here. Dumb scanners fail closed more often than they boast. A vault team can replace the regex later without moving the door.
// fail-closed-gate.mjs
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
const SECRET_RE = [
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
/(?:API[_-]?KEY|SECRET|TOKEN|PASSWORD)\s*[:=]\s*['"][^'"]{8,}['"]/i,
/AKIA[0-9A-Z]{16}/,
];
const DENY_NAMES = ['.env', '.env.local', 'credentials.json', 'id_rsa'];
export function pathDenied(filePath) {
const base = filePath.split(/[\\/]/).pop();
return DENY_NAMES.includes(base);
}
export function scanSecrets(text) {
return SECRET_RE.some((re) => re.test(text));
}
export function decideHop({ denied, leaked, offline, rttMs, budgetMs }) {
if (denied || leaked || offline) {
return { route: 'local', reason: 'fail_closed' };
}
if (rttMs == null || rttMs > budgetMs) {
return { route: 'local', reason: 'slow_or_unknown_link' };
}
return { route: 'remote', reason: 'burst_allowed' };
}
export async function inspectJob(filePath, opts = {}) {
const offline = opts.offline ?? false;
const rttMs = opts.rttMs ?? null;
const budgetMs = opts.budgetMs ?? 150;
const abs = resolve(filePath);
const denied = pathDenied(abs);
const text = denied ? '' : await readFile(abs, 'utf8');
const leaked = denied || scanSecrets(text);
const decision = decideHop({ denied, leaked, offline, rttMs, budgetMs });
return { file: abs, denied, leaked, offline, rttMs, ...decision };
}
The function never opens a socket by itself. That separation is the whole point of a bouncer. Transport stays in a different file you can audit.
Denied names never even get read into a prompt buffer. That extra lock costs almost nothing on disk. It blocks the usual accident of shipping .env in a bundle.
Exercise the gate on a dotenv path with this command. Paste nothing into a remote form for this check. The printed route should stay local on that path.
node --input-type=module -e "import { inspectJob } from './fail-closed-gate.mjs'; console.log(JSON.stringify(await inspectJob('.env', { rttMs: 40 })))"
Sample the wire once, then log it
Do not guess latency from a hallway story. Sample one HEAD request against a host you already trust. Store that number next to the route decision.
// probe-rtt.mjs
export async function probeRtt(url, timeoutMs = 800) {
const start = performance.now();
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
await fetch(url, { method: 'HEAD', signal: ctrl.signal });
return Math.round(performance.now() - start);
} catch {
return null;
} finally {
clearTimeout(timer);
}
}
Wire the probe to the gate with a second command. Pass null through when the HEAD fails. The bouncer must see that miss as a local vote.
node --input-type=module -e "import { probeRtt } from './probe-rtt.mjs'; import { inspectJob } from './fail-closed-gate.mjs'; const rttMs = await probeRtt('https://example.com'); console.log(JSON.stringify(await inspectJob('README.md', { rttMs })))"
A null result means the link failed the job. The gate then stays on disk without debate. Fail-closed beats a lucky retry that leaks a key.
A 150 ms budget is a lab default only. Tune it on your actual route later. Keep the budget in the JSON log, not in folklore.
Head probes measure setup cost, not model quality. They tell you if the truck is even in the driveway. They do not tell you if the crate will be packed well.
Tests that do not need the internet
A gate without tests is only a mood. The cases below use Node's built-in test runner. They never touch a network stack in CI.
// fail-closed-gate.test.mjs
import test from 'node:test';
import assert from 'node:assert/strict';
import { decideHop, scanSecrets, pathDenied } from './fail-closed-gate.mjs';
test('env path never leaves disk', () => {
assert.equal(pathDenied('/repo/.env'), true);
const d = decideHop({
denied: true,
leaked: false,
offline: false,
rttMs: 40,
budgetMs: 150,
});
assert.equal(d.route, 'local');
});
test('token assignment blocks the hop', () => {
assert.equal(scanSecrets("API_KEY='w3x4y5z6a7b8c9d0'"), true);
});
test('offline vetoes a fast clean file', () => {
const d = decideHop({
denied: false,
leaked: false,
offline: true,
rttMs: 20,
budgetMs: 150,
});
assert.equal(d.route, 'local');
assert.equal(d.reason, 'fail_closed');
});
test('clean fast online file may hop', () => {
const d = decideHop({
denied: false,
leaked: false,
offline: false,
rttMs: 60,
budgetMs: 150,
});
assert.equal(d.route, 'remote');
assert.equal(d.reason, 'burst_allowed');
});
test('unknown rtt stays local', () => {
const d = decideHop({
denied: false,
leaked: false,
offline: false,
rttMs: null,
budgetMs: 150,
});
assert.equal(d.route, 'local');
});
Run the suite with one command on a quiet laptop.
node --test fail-closed-gate.test.mjs
The leak fixture must stay local in every run. The clean fixture may hop only when the probe is fast. The offline fixture never hops, even on a perfect sample.
That contract is the reproducible artifact for this workflow. If a later change softens the door, the test fails first. Do not skip the suite to ship a friendlier remote path.
When burst on a free server is rational
Local CPU still loses on large, secret-free bursts. Public fixtures and generated tests fit that shape. Long refactors of sample code can fit it too.
The laptop stays cool while the crate rides the truck. The hop is rational only after redaction succeeds. Strip dotenv files, private keys, and customer dumps before pack.
If redaction throws, cancel the hop at once. Keep those bytes on the laptop disk. A partial redact is a leak with extra steps.
MonkeyCode can sit on that remote branch when the gate returns burst_allowed. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Free model access and a free server option are the claims used here.
Point the remote branch at that hop for a secret-free fixture. Keep the repo on disk after the burst. Use it only if you need a burst valve.
Do not read those lines as a quota or hardware promise. Those figures change without notice in this article. Measure your own jobs against your own log.
Who should not open the door
Air-gapped teams should leave the remote route compiled out. The offline flag already votes no on every job. Do not override it for a stage demo.
Regulated trees with customer data should stay on disk. A secret regex is not a DLP product. It will miss tokens in binaries and screenshots.
Tight inner loops should never take the truck. Formatters, typechecks, and unit tests still want milliseconds. They do not want a TLS handshake on each file.
If nobody can read the packed prompt, nobody should send it. A bouncer who cannot see the crate must stop the truck. Convenience is not allowed as a fourth check.
Log every decision like a brake
Write one JSON line per inspected job. Record path, reason, rtt_ms, budget_ms, and timestamp. You will argue with this file during the next incident.
The log is not a scoreboard for remote usage. It is a brake pedal on a downhill road. When remote volume grows, the log shows whether the wire earned the trip.
False negatives in the scanner remain the real hazard. A token in a binary can slip through quietly. Keep the denylist of file names as a second lock.
False positives waste a free hop and that is acceptable. Local work still finishes the original job on disk. A wasted truck is cheaper than a leaked key.
A flaky probe pins work to disk by design. Prefer a stuck laptop over a chatty leak. Retry policy belongs in the probe file, not in the bouncer.
// log-decision.mjs
import { appendFile } from 'node:fs/promises';
export async function logDecision(fileUrl, row) {
const line = JSON.stringify({ ts: new Date().toISOString(), ...row }) + '\n';
await appendFile(fileUrl, line);
}
Append a line after every inspect with one more command. Read the file during the next argument about remote volume. The brake pedal only works if the line exists.
node --input-type=module -e "import { inspectJob } from './fail-closed-gate.mjs'; import { logDecision } from './log-decision.mjs'; const row = await inspectJob('README.md', { rttMs: 60 }); await logDecision(new URL('./hop-decisions.jsonl', import.meta.url), row);"
That JSON logger is kept boring on purpose. Boring logs still get read during real failures. Fancy dashboards often hide the brake pedal.
Limits of the method
This gate does not replace human code review. It does not prove generated patches are correct. It only decides where bytes may travel today.
Remote inference can still be wrong on a clean crate. Treat every output as a candidate diff on disk. Run tests on disk before any merge.
The method assumes Node 18 or newer on the laptop. It assumes fail-closed is acceptable to the team. It assumes nobody pastes secrets to bypass the scan.
It also assumes the remote endpoint stays optional. If a vendor is mandatory, this runbook does not apply. Choose a controlled private path instead of this door.
Clock math is one-sided on purpose in the probe. You compare your sample to your own budget. You do not need synchronized hosts for that comparison.
Redaction scripts fail in boring ways as well. Filename denylists miss secrets copied into notes files. When in doubt, keep the route on local disk.
The door stays shut until the crate is clean
Disk remains the source of truth for secrets and tight loops. The wire is a burst valve with a lock on the handle. Open that lock only after leak, latency, and link all pass.
Keep the keys in the bowl by default. Rent the truck when the crate is clean. The bouncer still does not sing on stage.
Top comments (1)
Separating the gate from the transport is the design choice I'd defend most here. The scanner script never opens a socket, so auditing it is a read-only exercise, and swapping in a real secret detector later doesn't move the decision point. The
rttMs == nullbranch returninglocalrather thanremoteis the correct default too — an unmeasured link is not a cheap link.Where I'd expect the 150 ms budget to bite is the sampling itself: one round trip tells you almost nothing about a connection that's about to do twenty hops. We run three samples and take the median before allowing remote, otherwise a single lucky ping routes a whole burst onto a link that was about to die. How are you measuring the RTT — TCP connect to the API host, or an actual request you'd be making anyway?