DEV Community

Quinn Li
Quinn Li

Posted on

Letter to Sunday-Me: Pin the Remote Job Before You Prompt

Dear Sunday-me,

You burned a full day on a remote agent. The model was not the real fault. The unpinned job was the fault.

You chased a flaky Node parser. You wanted cheaper iteration than your laptop. You pointed a free remote model at a moving tree. Then the tree changed under you.

This letter is the workflow I needed. Three concrete mistakes. One sealed job pin. A replay you can rerun on Monday.

The scene that ate the day

Agent threads were loud again this week. Most of them argued about model skill. Almost none of them pinned the bytes the model saw.

Your repo was a small Node CLI. One fixture failed only on CI. You pasted the stack into a chat box. You asked a remote model to fix the parser.

It returned a patch. You applied that patch locally. CI still failed after lunch. You prompted again with fresher logs.

The second patch reverted the first change. Noon was already gone. The failure was drift, not intellect. The remote host never saw the same bytes twice.

Mistake 1: You streamed a live working tree

You archived the live folder. node_modules rode along for the ride. .env.local rode along too. An unsaved editor buffer never made the archive.

The remote compile graph diverged from CI. Cache keys diverged after that. File timestamps diverged across two clocks.

Do not ship the working tree. Ship a digest of tracked files only. If git does not see a path, the job does not see it either.

Mistake 2: You smuggled implicit credentials

A free server is not your laptop. It does not hold your SSH agent. It does not hold your npm token.

You pasted a token into the prompt as a shortcut. That token then lived inside a transcript. Transcripts get stored by default. Transcripts get reused as later context.

Pass allowlisted env through a gate file. Never pass secrets through prose. Rotate anything that already touched a prompt.

Mistake 3: You debugged by re-prompting

Each new prompt mutated the story. You had no stable job id. You had no input hash. You could not replay the failing step.

A remote loop without replay is folklore. Folklore dies in a Monday standup. Freeze the job first. Change one variable only. Rerun the same bytes.

Workflow: pin, ship, then replay

Use this sequence on a throwaway branch. Do not skip the pin. Label every snippet below as a proposal until you run it.

Step 1 — Write a job spec on disk

Keep the spec small. Keep the spec diffable. Keep secrets out of the file.

{
  "job_id": "parser-ci-2026-09-11",
  "goal": "make tests/parser.spec.mjs pass",
  "tracked_only": true,
  "allow_env": ["CI", "NODE_ENV"],
  "deny_env": ["*_TOKEN", "*SECRET*", "AWS_*"],
  "commands": {
    "install": "npm ci",
    "test": "node --test tests/parser.spec.mjs"
  },
  "artifact": "patch.diff"
}
Enter fullscreen mode Exit fullscreen mode

Treat this file as a template. Rename fields for your repo. Do not place live tokens in it.

Step 2 — Pin tracked files to a digest

Hash git-tracked files only. Ignore dirty buffers. Ignore local caches.

// pin-job.mjs — proposal, run against your own repo
import { execFileSync } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";

const spec = JSON.parse(readFileSync("job.spec.json", "utf8"));
const files = execFileSync("git", ["ls-files", "-z"], { encoding: "buffer" })
  .toString("utf8")
  .split("\0")
  .filter(Boolean);

const h = createHash("sha256");
for (const f of files.sort()) {
  h.update(f);
  h.update("\0");
  h.update(readFileSync(f));
}

const pin = {
  job_id: spec.job_id,
  run_id: randomUUID(),
  tree_sha256: h.digest("hex"),
  spec,
};

writeFileSync("job.pin.json", JSON.stringify(pin, null, 2));
console.log(pin.tree_sha256);
Enter fullscreen mode Exit fullscreen mode

Run these two commands together. Compare both outputs later.

node pin-job.mjs
git rev-parse HEAD
Enter fullscreen mode Exit fullscreen mode

If the digest changes, you lack a replay. Stop the remote call. Commit or revert first. Then pin again.

Step 3 — Strip env before upload

Build a tight allowlist. Reject names that look like secrets. Cap value length so dumps cannot hide in env.

// gate-env.mjs — proposal
const allow = new Set(["CI", "NODE_ENV"]);
const deny = [/_TOKEN$/i, /SECRET/i, /^AWS_/i];

export function gateEnv(src, allowNames = allow) {
  const out = {};
  for (const [k, v] of Object.entries(src)) {
    if (!allowNames.has(k)) continue;
    if (deny.some((re) => re.test(k))) continue;
    if (typeof v !== "string") continue;
    if (v.length > 256) continue;
    out[k] = v;
  }
  return out;
}
Enter fullscreen mode Exit fullscreen mode

Keep a unit check next to the gate. Run it before every upload.

import assert from "node:assert/strict";
import { gateEnv } from "./gate-env.mjs";

const got = gateEnv({
  CI: "1",
  NODE_ENV: "test",
  NPM_TOKEN: "deadbeef",
  AWS_SECRET_ACCESS_KEY: "nope",
});

assert.deepEqual(got, { CI: "1", NODE_ENV: "test" });
console.log("gate-env ok");
Enter fullscreen mode Exit fullscreen mode

Step 4 — Run the model on a free remote host

Keep the laptop as the gate. Keep the remote host as an untrusted worker. That split is the whole method.

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

MonkeyCode is relevant here only as that worker. Operator-supplied facts stop at free model access and a free server option. This article does not name models. It does not claim quotas, hardware, duration, or permanence.

Pack tracked files only. Leave .git on the laptop. Leave ungated env on the laptop.

git archive --format=tar.gz --output job-src.tgz HEAD
# proposal: upload job.pin.json and job-src.tgz
# proposal: remote runs npm ci && node --test
# proposal: remote returns patch.diff and transcript.json
Enter fullscreen mode Exit fullscreen mode

The model may edit files on that host. It must not touch your laptop tree yet.

Step 5 — Demand a replay bundle

The remote side should return four files. Reject the run if any file is missing.

  1. patch.diff — unified diff only.
  2. transcript.json — prompts, tool names, exit codes.
  3. tree_sha256 — echo of the pin you sent.
  4. test-output.txt — raw test log.

Reject the bundle on digest mismatch. Reject diffs that touch denied paths. Reject transcripts that echo denied env names.

// accept-bundle.mjs — proposal
import { readFileSync } from "node:fs";
import assert from "node:assert/strict";

const pin = JSON.parse(readFileSync("job.pin.json", "utf8"));
const bundle = JSON.parse(readFileSync("transcript.json", "utf8"));
const diff = readFileSync("patch.diff", "utf8");

assert.equal(bundle.tree_sha256, pin.tree_sha256);
assert.equal(bundle.job_id, pin.job_id);
assert.doesNotMatch(diff, /\.env/);
assert.doesNotMatch(JSON.stringify(bundle), /TOKEN|SECRET|AWS_/i);
console.log("bundle accepted");
Enter fullscreen mode Exit fullscreen mode

Step 6 — Apply the patch after a local contract

Do not trust a green remote log. Replay the patch on a clean checkout. Use a worktree so your dirty files stay untouched.

git worktree add /tmp/replay HEAD
cd /tmp/replay
git apply --check /path/to/patch.diff
git apply /path/to/patch.diff
npm ci
node --test tests/parser.spec.mjs
Enter fullscreen mode Exit fullscreen mode

If this fails, you still have the pin. Rerun the same job id. Do not invent a new prompt from memory.

Decision table

Use the table as a method. It is not a benchmark. Fill the last column with your runtime names after you measure them.

Symptom Likely cause Next action
Remote tests pass, local tests fail Unpinned dirty tree Re-run node pin-job.mjs
Model asks for tokens Prompt leak or missing gate Rotate the token, tighten allowlist
Second run "fixes" a different bug No job id / no digest Freeze job.pin.json, replay
Diff edits lockfiles unexpectedly Broad tool access Limit commands in the spec
Transcript repeats secrets Env copied into chat Delete transcript, treat as incident

Read the table left to right. Do not jump to a new prompt. The next action is always a pin, a gate, or a replay.

What this does not solve

This workflow does not make a model truthful. It makes failures comparable. Comparable failures are the only kind you can budget.

It does not replace code review. A sealed job can still ship a wrong patch. A green test file is not a product spec.

It does not make a free remote host private. Treat that host as shared. Assume transcripts are durable. Assume disk is not yours.

It does not bound token spend. Free access is not an unbounded budget. Cap loops in the spec. Stop after a fixed tool-call count.

Who should skip this approach:

  • Teams that cannot isolate secrets from a vendor host.
  • Work that must stay air-gapped.
  • Incidents whose failing state exists only on one laptop.
  • People still learning git archive and worktrees. Learn those first.

A 30-minute drill

Run this drill on a throwaway branch. Label it a drill. Do not use production creds.

  1. Pick one failing test.
  2. Write job.spec.json with two commands only.
  3. Pin with node pin-job.mjs.
  4. Gate env with the unit check above.
  5. Ship the archive to a free remote worker.
  6. Accept or reject the bundle with accept-bundle.mjs.
  7. Replay the patch inside a worktree.

Write down the two hashes. If they diverge, the day is already saved. You found the real bug: drift.

Sunday-me, the model was a noisy coworker. The pin was the contract. Start with the contract. If you later need a remote worker with free model access and a free server option, use MonkeyCode only after that gate exists.

Top comments (0)