DEV Community

Riley Wu
Riley Wu

Posted on

Keep the Key on Disk Until Burst Work Pays

A remote hop is a cost, not a default. Secrets belong on disk, not on the wire. Offline work stays local until a clean burst justifies rent.

Most agent loops invert that order. They post context on the first tool call. The wire then holds both the prompt and the credential.

That pattern fails in three coupled ways. Latency compounds on every round trip. Secrets leak into logs you do not own. A dead link kills work that could have stayed on disk.

The fix is a gate, not a slogan. Scan the payload before DNS. Probe the link without the body. Send work only when the burst is large and the key stays home.

The split most loops skip

Think of the laptop as a locked workshop. The cloud is a rented mill down the road. You do not haul the safe to the mill. You haul lumber that carries no serial numbers.

Agent frameworks blur that line on purpose. A tool function looks local in the editor. The HTTP client still leaves the box. One header can lift an API token from .env and park it in a vendor log.

Offline is the second trap, not a rare edge. A café radio drop should not freeze a refactor. Local CPU can still lint, grep, and patch. The mill can wait until the radio returns.

A free remote server still has a narrow role. Large summaries burn local fans for little gain. A scrubbed corpus can ride a cheap hop. The key must never ride with that corpus.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option. Those two facts matter only after the gate below passes. They do not replace a local secret check, and they do not make a shared hop suitable for regulated data.

A preflight that refuses dirty hops

The following Node script is a worked example. It is not a benchmark and not a security audit. Run it as a gate in front of any remote client you already control.

// preflight-hop.mjs — example gate, not a security audit
import fs from "node:fs";
import http from "node:http";
import https from "node:https";
import { URL } from "node:url";

const SECRET_RE = [
  /api[_-]?key\s*[:=]\s*['"][^'"]+['"]/i,
  /secret\s*[:=]\s*['"][^'"]+['"]/i,
  /bearer\s+[a-z0-9._\-]{20,}/i,
  /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
  /AKIA[0-9A-Z]{16}/,
];

const ENV_HINT = /\b(process\.env|Deno\.env|os\.environ)\b/;

export function findSecretHits(text) {
  const hits = [];
  for (const re of SECRET_RE) {
    if (re.test(text)) hits.push(re.source);
  }
  if (ENV_HINT.test(text)) hits.push("env-reference");
  return hits;
}

export function payloadBytes(text) {
  return Buffer.byteLength(text, "utf8");
}

// Example threshold only. Tune on your own traces.
const BURST_BYTES = 24 * 1024;

export function classifyHop({ text, online, allowRemote }) {
  const hits = findSecretHits(text);
  if (hits.length) {
    return { action: "local-only", reason: "secret-pattern", hits };
  }
  if (!online) {
    return { action: "local-only", reason: "offline" };
  }
  if (!allowRemote) {
    return { action: "local-only", reason: "policy" };
  }
  if (payloadBytes(text) < BURST_BYTES) {
    return { action: "local-only", reason: "small-burst" };
  }
  return { action: "remote-ok", reason: "clean-burst" };
}
Enter fullscreen mode Exit fullscreen mode

The classifier is boring on purpose. Boring gates survive weekends. Fancy scorers tend to ship the key because a score looks like permission.

A hop probe must not include the prompt. Ping a health path with an empty body. Time that ping and store the result on disk. Reuse it for the next loop tick instead of probing on every token.

export function probeLink(rawUrl, timeoutMs = 800) {
  return new Promise((resolve) => {
    let settled = false;
    const u = new URL(rawUrl);
    const lib = u.protocol === "https:" ? https : http;
    const t0 = Date.now();
    const req = lib.request(
      {
        hostname: u.hostname,
        port: u.port || (u.protocol === "https:" ? 443 : 80),
        path: "/health",
        method: "HEAD",
        timeout: timeoutMs,
      },
      (res) => {
        if (settled) return;
        settled = true;
        resolve({ online: res.statusCode < 500, rttMs: Date.now() - t0 });
      }
    );
    req.on("timeout", () => {
      req.destroy();
      if (!settled) {
        settled = true;
        resolve({ online: false, rttMs: timeoutMs });
      }
    });
    req.on("error", () => {
      if (!settled) {
        settled = true;
        resolve({ online: false, rttMs: -1 });
      }
    });
    req.end();
  });
}
Enter fullscreen mode Exit fullscreen mode

Wire those two functions in the agent entrypoint. Read the planned prompt from disk first. Classify it before any socket opens. Probe only if classification already says remote-ok.

import fs from "node:fs";
import { classifyHop, probeLink } from "./preflight-hop.mjs";

export async function planHop(promptPath, remoteHealthUrl) {
  const text = fs.readFileSync(promptPath, "utf8");
  const first = classifyHop({ text, online: true, allowRemote: true });
  if (first.action !== "remote-ok") return first;

  const link = await probeLink(remoteHealthUrl);
  return classifyHop({
    text,
    online: link.online,
    allowRemote: true,
  });
}
Enter fullscreen mode Exit fullscreen mode

That order matters more than the regex list. Secrets fail closed before DNS. Offline fails closed before tokens spend. Small bursts stay on the fan you already paid for.

What a free server actually wins

A free remote hop wins on bulk, not on chat. Fold a large log into a summary after hosts are stripped. Cluster stack traces that no longer name customers. Draft a changelog from public diffs that already left the repo.

It loses when the file is a keyring. It loses when the train has no radio. It loses when the patch is twenty lines. Local grep still beats a long RTT on tiny work, even when the remote mill is priced at zero.

Zero price is not zero risk. A mill you did not rack still logs. A mill you did not rack still queues. Your gate still owns the key, and the mill still sees whatever you actually posted.

Do not treat free capacity as a reason to skip the scan. Treat it as burst overflow for already-clean text. If the classifier returns local-only, the mill does not get a vote.

If you need a remote burst without standing up hardware, MonkeyCode's free model access and free server option can sit behind planHop. Use them only after the local checks pass.

A tiny test you can rerun

Label this as a test plan, not a published metric. Save three fixtures beside the gate. Run the classifier and expect three different actions. The point is the branch table, not a scoreboard.

// preflight-hop.test.mjs — example assertions
import assert from "node:assert/strict";
import { classifyHop } from "./preflight-hop.mjs";

const secret = 'const k = "AKIAIOSFODNN7EXAMPLE";\n';
const tiny = "refactor the names in src/util.js\n";
const burst = "x".repeat(30 * 1024);

assert.equal(
  classifyHop({ text: secret, online: true, allowRemote: true }).action,
  "local-only"
);
assert.equal(
  classifyHop({ text: tiny, online: true, allowRemote: true }).reason,
  "small-burst"
);
assert.equal(
  classifyHop({ text: burst, online: false, allowRemote: true }).reason,
  "offline"
);
assert.equal(
  classifyHop({ text: burst, online: true, allowRemote: true }).action,
  "remote-ok"
);

console.log("preflight fixtures passed");
Enter fullscreen mode Exit fullscreen mode

Run it with node preflight-hop.test.mjs. Add a fourth fixture that embeds process.env.OPENAI_KEY if your agents quote env names. The ENV_HINT branch exists because many prompts leak by reference, not by value.

Keep the fixtures in git. A gate without fixtures drifts. The next intern will raise BURST_BYTES and ship a .pem by accident.

Limitations the gate will not hide

Regex is not a vault, and it never will be. It misses custom headers and binary keystores. It misses secrets split across chunked tool calls. Rotate keys if a hop already fired with a dirty body.

The health HEAD can lie in ordinary networks. A 200 on /health does not mean spare capacity. A captive portal can look online while eating POST bodies. Keep a manual override on disk for travel days.

The byte threshold is an example, not science. Twenty-four kilobytes is a placeholder for “too small to rent the mill.” Measure your own local tokens per second if you need a real cutoff. Do not copy this number into policy documents.

This gate does not encrypt the disk under the prompt. Full-disk encryption is a separate control. So is OS keychain use. So is refusing to paste .env into any chat box, local or remote.

Shared free servers also do not provide tenancy you can point at in an audit. If a contract requires a VPC, this hop is the wrong mill. Price zero does not change the isolation story.

Who should skip this approach

Skip it if your runtime cannot read the prompt before send. Some hosted agent UIs fire the HTTP call inside a black box. A gate you cannot insert is theater, and theater ships keys.

Skip it if the workload is already a public corpus. Scrubbing empty air wastes a loop tick. Ship the public tarball and keep the keyring on the laptop.

Skip it if you need guaranteed isolation for customer data. A free shared server is still shared. Regulated transcripts should not take that hop. Use a contract, a private endpoint, and a review, not a health check.

Local-first is a default, not a vow. Keep the key on disk. Keep the loop alive when the radio dies. Rent the mill only for a clean burst that earned the wire.

Top comments (0)