On a Tuesday afternoon, a staff engineer watched a local coding agent chew through a refactor on a four-year-old payments service. The laptop stayed warm, the uncommitted tree stayed dirty, and the .env file never left the disk that already held it. Later that night a teammate queued a bulk fixture generator because the laptop could not finish the run before a standup demo. They rsynced the entire working copy to a borrowed cloud box, including the secret file that the daytime session had treated as immovable.
The morning review blamed a missing split between work that must stay local and work allowed to leave. Interactive refactors, dirty trees, and live secrets belong on the laptop that already holds them. Clean batch jobs with an explicit latency budget can use another machine when the laptop is the bottleneck. This article records that split as a manifest, a classifier, and a small test plan rather than as tribal memory.
Secret gravity is not payload mass
Secret gravity describes how hard a path pulls toward the laptop, regardless of CPU need. A .env file, an SSO cookie, a production dump, and an uncommitted patch all have high gravity even when they are tiny. Payload mass describes how expensive the job is to move and to run, including context size, tool fan-out, and wall-clock length. Mixing those two axes is how teams justify copying a whole home directory because a job feels heavy.
A heavy job with high gravity still stays local, or it is reduced until gravity drops. A light job with low gravity can leave even when the laptop is idle, because isolation has value of its own. Public demos this week keep celebrating agents that never leave the user's environment, and that instinct is sound for interactive turns. Batch compute is a different plane, and it should be admitted only after the payload is named.
When a remote plane actually wins
A remote plane wins for clean, repeatable batch work that does not need the dirty tree. Typical winners include fixture generation from a committed spec, a contract suite pinned to a known SHA, and summaries of already-redacted logs. It also wins when the laptop must stay interactive, because a long generation should not steal the editor's cores during review. Offline windows still lose: if the network is gone, the remote plane does not exist, and the local agent must keep a degraded path.
Free model access and a free server option matter only after those gates pass. MonkeyCode offers free model access and a free server option for that second plane, which is worth evaluating after a job is classified as low gravity. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The classifier below remains useful if that product is removed, because residency rules do not depend on a vendor name.
A five-step placement workflow
1. Write a job manifest before any file copy
The operator records intent in a YAML file that lists inputs by path, not by globbing the whole repository. Declared inputs are the only files later eligible for packing. Anything omitted is treated as local-only residue, including editor swap files and ignored secret stores. The manifest is committed or at least reviewed, because an unreviewed pack list is how yesterday's .env travels again.
# job_manifest.yaml
id: fixture-gen-2026-09-23
intent: batch_generate
sha: 7c1e9aa
inputs:
- specs/fixtures.md
- contracts/orders.v2.json
outputs:
- testdata/generated/
max_payload_bytes: 1048576
allow_remote: true
requires_dirty_tree: false
requires_interactive: false
2. Score secret gravity from path patterns
Gravity scoring runs on declared inputs and on the working tree if the job claims it needs dirt. High-gravity matches fail closed; they do not warn and continue. Patterns below are a starting set, not a compliance program, and each team should extend them from its own incident log. A job that needs a high-gravity path must be rewritten to a redacted scaffold before remote placement is considered.
3. Score payload mass from declared inputs only
Mass is the sum of declared file sizes plus a crude estimate of output budget. Scanning the entire git worktree for mass is forbidden, because that scan reintroduces undeclared secrets as "context." If the declared set exceeds the cap, the job is split or it stays local. Mass never overrides gravity: a two-kilobyte secret file still pins the job to the laptop.
4. Apply the fail-closed table
The table in the next section is the policy. Interactive jobs stay local. Jobs that require an uncommitted tree stay local. Jobs that fail gravity checks stay local. Only batch jobs with low gravity, declared inputs, and a known SHA are eligible for a remote plane, including a free server.
5. Copy a scaffold, never a home directory
Eligible jobs pack only listed files into an archive that is hashed before upload. The remote side receives the archive and the SHA, not SSH access to the laptop. Results return as output files that must match the declared output prefix. Anything else is discarded, because surprise paths are how secrets re-enter the story.
Artifact: a classifier the tests can refuse
The script below is a proposal for a local gate. It does not contact a network, and it prints LOCAL or REMOTE_ELIGIBLE on stdout. Operators should run it in a clone that contains dummy secret names, not production material.
#!/usr/bin/env python3
"""Place an agent job: LOCAL or REMOTE_ELIGIBLE. Proposal, not production policy."""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
import yaml
GRAVITY = re.compile(
r"(?:^|/)("
r"\\.env(?:\\..*)?|\\.netrc|id_rsa|id_ed25519|"
r".*\\.(pem|p12|keystore)|secrets?\\.ya?ml|"
r"credentials\\.json|\\.npmrc|\\.pypirc"
r")$",
re.I,
)
def load_manifest(path: Path) -> dict:
data = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(data, dict) or "inputs" not in data:
raise ValueError("manifest must be a mapping with inputs")
return data
def gravity_hits(root: Path, rels: list[str]) -> list[str]:
hits = []
for rel in rels:
if GRAVITY.search(rel.replace("\\\\", "/")):
hits.append(rel)
continue
abs_path = (root / rel).resolve()
if not abs_path.is_file():
hits.append(f"{rel}:missing")
continue
# Tiny content sniff for assignment-like secret lines.
text = abs_path.read_text(encoding="utf-8", errors="ignore")[:8000]
if re.search(r"(API_KEY|SECRET|PRIVATE_KEY)\\s*=", text):
hits.append(f"{rel}:content")
return hits
def payload_bytes(root: Path, rels: list[str]) -> int:
total = 0
for rel in rels:
abs_path = (root / rel).resolve()
if abs_path.is_file():
total += abs_path.stat().st_size
return total
def place(root: Path, manifest: dict) -> str:
if manifest.get("requires_interactive"):
return "LOCAL"
if manifest.get("requires_dirty_tree"):
return "LOCAL"
if not manifest.get("allow_remote"):
return "LOCAL"
inputs = list(manifest["inputs"])
hits = gravity_hits(root, inputs)
if hits:
return "LOCAL"
mass = payload_bytes(root, inputs)
cap = int(manifest.get("max_payload_bytes", 0))
if cap and mass > cap:
return "LOCAL"
if not manifest.get("sha"):
return "LOCAL"
return "REMOTE_ELIGIBLE"
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", type=Path, default=Path("."))
parser.add_argument("--manifest", type=Path, required=True)
args = parser.parse_args()
decision = place(args.root, load_manifest(args.manifest))
print(decision)
return 0 if decision else 2
if __name__ == "__main__":
sys.exit(main())
A companion test file pins the fail-closed cases. These tests are meant to run on fixture files in testdata/place_job/, not on a live application repository.
# test_place_job.py
from pathlib import Path
from place_job import place
ROOT = Path("testdata/place_job")
def test_env_file_never_leaves():
manifest = {
"inputs": [".env"],
"allow_remote": True,
"requires_dirty_tree": False,
"requires_interactive": False,
"sha": "abc",
"max_payload_bytes": 10_000_000,
}
assert place(ROOT, manifest) == "LOCAL"
def test_committed_spec_may_leave():
manifest = {
"inputs": ["specs/fixtures.md"],
"allow_remote": True,
"requires_dirty_tree": False,
"requires_interactive": False,
"sha": "7c1e9aa",
"max_payload_bytes": 10_000_000,
}
assert place(ROOT, manifest) == "REMOTE_ELIGIBLE"
def test_interactive_turn_stays():
manifest = {
"inputs": ["specs/fixtures.md"],
"allow_remote": True,
"requires_dirty_tree": False,
"requires_interactive": True,
"sha": "7c1e9aa",
"max_payload_bytes": 10_000_000,
}
assert place(ROOT, manifest) == "LOCAL"
Commands that keep the loop honest look like the following. The pack step is shown as a dry run so a reviewer can read the file list before any copy occurs.
python3 -m pip install pyyaml pytest
pytest -q test_place_job.py
python3 place_job.py --root . --manifest job_manifest.yaml
# Expected on a clean spec job: REMOTE_ELIGIBLE
tar -cvf /tmp/agent-scaffold.tar -T declared_inputs.txt
sha256sum /tmp/agent-scaffold.tar
Decision table
| Job class | Secret gravity | Payload mass | Network | Placement |
|---|---|---|---|---|
| Interactive edit in the current buffer | High by default | Low | Irrelevant | Local laptop |
| Uncommitted refactor that still compiles | High (dirty tree) | Medium | Irrelevant | Local laptop |
| Fixture generation from a committed spec | Low if paths are clean | Medium or high | Required | Remote eligible |
| Contract tests on a pinned SHA | Low | Medium | Required | Remote eligible |
| Redacted log summary | Low after redaction | High | Required | Remote eligible |
Anything touching .env, keys, dumps |
High | Any | Irrelevant | Local laptop |
| Same jobs during a network brownout | As above | As above | Absent | Local degraded path |
The table is the policy artifact. Teams should print it next to the agent runner, because a model prompt will not remember last week's incident. Remote eligible still means eligible, not mandatory; a quiet laptop may run the batch locally to avoid a round trip.
Measuring the hop without trusting folklore
Latency folklore is how secrets travel: someone claims the laptop is too slow, so the whole tree is copied. Measure the job that is actually proposed, using the declared inputs only. The snippet below is a method, not a published benchmark, and operators must fill times from their own machines.
# method only — record numbers locally; do not paste secrets into the command
/usr/bin/time -f 'local_wall_sec=%e' python3 generate_fixtures.py --spec specs/fixtures.md
# After a classifier pass, time the remote path the same way on the packed archive.
# Compare: pack_sec + upload_sec + remote_wall_sec + download_sec versus local_wall_sec.
# If pack_sec alone exceeds local_wall_sec, the remote plane is not winning.
Three clocks matter, and they are easy to confuse in an incident channel. Local wall clock is the editor-facing cost. Pack and transfer clock is the secrecy boundary, because that is when bytes leave the disk. Remote wall clock is only meaningful after the first two clocks are small and the gravity check is green. If transfer dominates, keep the job local or shrink the scaffold until transfer is boring.
Limitations
The classifier is pattern-based and will miss secrets with novel names, binary blobs, and screenshots of dashboards. It does not prove that a committed spec is free of customer data, and it does not replace a real secret scanner in CI. YAML manifests can be edited to lie; the tests catch fixtures, not a determined bypass. A free server is a convenience plane, not a production SLA, and it should not hold the only copy of generated artifacts.
Word-count and token-count folklore is omitted on purpose, because those figures go stale and are easy to invent. Placement should follow gravity, mass, and measured wall clocks on the job at hand. Teams that need audited isolation, data-processing agreements, or regional pinning must use a controlled runner, not an opportunistic free box.
Who should not use this split
This workflow is a poor fit for regulated datasets that cannot leave the endpoint under any packing story. It is also a poor fit for agents whose only mode is interactive pair editing, because those turns were never candidates for a remote plane. Operators who cannot name a SHA, a declared input list, and an output prefix should keep every byte on the laptop. Treat the remote option as an overflow lane for clean batch work, not as a second home directory.
Top comments (0)