DEV Community

Dakota Liu
Dakota Liu

Posted on

Pack a Redacted Job. Never Upload the Working Tree to a Remote Agent.

Never give a remote agent your working tree. Pack a redacted job. Prove the returned file on your laptop before it touches main. That is the whole article.

A coding agent on someone else's machine is not a teammate. It is an untrusted compiler with a chat window. So why would you upload .env, a kubeconfig, and three years of scripts/ just to get a slugify helper?

This is a from-zero walkthrough. Each stage ends with a command you can run. If a stage fails, stop. Do not "just send the zip."

The boring failure

The failure is not sci-fi. You archive the repo. You forget deploy/staging.pem. You forget the customer dump under tmp/. Then a remote loop reads files you never meant to share.

Can a model invent a useful function without your secrets? Usually, yes. Does your working tree contain secrets anyway? Also yes. Treat those two facts as the design constraint, not as a lecture about "being careful."

What you will have when this works

A tiny local demo repo. An allowlist. A packer that refuses to ship secret-shaped bytes. A manifest with SHA-256 for every packed file. A contract test that the returned module must pass on your machine. No cloud dashboard required for the gate. The gate is yours.

I am not claiming this makes a remote runner safe. I am claiming it makes the default upload path fail closed.

Stage 1: Create a throwaway demo, on purpose

Do this in an empty directory. Do not point the packer at your real product tree until the packer rejects a planted key. Seriously. Plant the key first.

mkdir -p /tmp/jobpack-demo/{src,secrets,tmp}
cd /tmp/jobpack-demo

cat > src/app.py << 'PY'
"""Tiny app. The remote agent should not need this file, but we keep it local."""
print("local-only app")
PY

cat > src/README.md << 'MD'
Write src/slugify.py with a function slugify(text: str) -> str.
Lowercase. Strip edges. Non-alphanumeric runs become one hyphen.
Empty input returns "". ASCII only for this exercise.
MD

cat > secrets/dev.env << 'ENV'
API_KEY=sk_live_this_is_fake_but_must_never_pack
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
ENV

echo "customer-dump" > tmp/export.csv
git init -q
Enter fullscreen mode Exit fullscreen mode

Verify: test -f secrets/dev.env && test -f src/README.md && echo ok-stage-1

If that does not print ok-stage-1, you are not in the demo directory. Fix the path before you write any Python.

Stage 2: Write the allowlist like you mean it

An allowlist is not a .gitignore with extra steps. .gitignore decides what you commit. The allowlist decides what a stranger's process may read. Different question.

cat > allowlist.txt << 'TXT'
src/README.md
TXT

cat > secret_patterns.txt << 'TXT'
(?i)api[_-]?key\s*=
(?i)secret[_-]?access[_-]?key
BEGIN (RSA |OPENSSH |EC )?PRIVATE KEY
sk_live_
AKIA[0-9A-Z]{16}
TXT
Enter fullscreen mode Exit fullscreen mode

Why is src/app.py missing? Because the remote job does not need it. If the agent needs more context later, you add a path on purpose. You do not "include src/ to be safe." Inclusive packing is how .env learns to fly.

Verify: wc -l allowlist.txt secret_patterns.txt

You want one allowlisted path and a handful of patterns. If allowlist.txt is empty, the packer should refuse. Empty means "I forgot," not "ship nothing and call it security."

Stage 3: Build a packer that names every byte

This script is the artifact. It copies only allowlisted files, hashes them, and writes jobpack/manifest.json. It does not talk to a model. Keep the model out of the packing loop. Why let a generator decide what you are willing to disclose?

# pack_job.py
from __future__ import annotations

import hashlib
import json
import re
import sys
import tarfile
from pathlib import Path

ROOT = Path.cwd()
ALLOW = (ROOT / "allowlist.txt").read_text(encoding="utf-8").splitlines()
ALLOW = [line.strip() for line in ALLOW if line.strip() and not line.startswith("#")]
PATTERNS = [
    re.compile(p) for p in (ROOT / "secret_patterns.txt").read_text(encoding="utf-8").splitlines()
    if p.strip() and not p.startswith("#")
]
OUT = ROOT / "jobpack"
TAR = ROOT / "jobpack.tar.gz"


def fail(msg: str) -> None:
    print(f"FAIL: {msg}", file=sys.stderr)
    raise SystemExit(1)


def scan(text: str, rel: str) -> None:
    for pat in PATTERNS:
        if pat.search(text):
            fail(f"secret-shaped bytes in {rel} matching /{pat.pattern}/")


def main() -> None:
    if not ALLOW:
        fail("allowlist.txt is empty")
    if OUT.exists():
        fail("jobpack/ already exists; delete it on purpose")
    OUT.mkdir()
    files = []
    for rel in ALLOW:
        src = ROOT / rel
        if not src.is_file():
            fail(f"allowlisted path missing: {rel}")
        text = src.read_text(encoding="utf-8")
        scan(text, rel)
        dest = OUT / rel
        dest.parent.mkdir(parents=True, exist_ok=True)
        dest.write_text(text, encoding="utf-8")
        digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
        files.append({"path": rel, "sha256": digest, "bytes": len(text.encode("utf-8"))})
    manifest = {"files": files, "file_count": len(files)}
    (OUT / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
    with tarfile.open(TAR, "w:gz") as tar:
        tar.add(OUT, arcname="jobpack")
    print(json.dumps(manifest, indent=2))
    print(f"wrote {TAR}")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run it.

python3 pack_job.py
Enter fullscreen mode Exit fullscreen mode

Verify:

python3 - << 'PY'
import json, tarfile
from pathlib import Path
m = json.loads(Path("jobpack/manifest.json").read_text())
assert m["file_count"] == 1
assert m["files"][0]["path"] == "src/README.md"
assert Path("jobpack.tar.gz").is_file()
with tarfile.open("jobpack.tar.gz", "r:gz") as tar:
    names = tar.getnames()
assert "jobpack/src/README.md" in names
assert not any("dev.env" in n or "export.csv" in n for n in names)
print("ok-stage-3")
PY
Enter fullscreen mode Exit fullscreen mode

One file in. Secrets out. If file_count is not 1, your allowlist drifted. Fix the list, do not patch the tar.

Stage 4: Plant a leak and watch the packer refuse

A gate you never fail is not a gate. Add the env file to the allowlist on purpose. Then demand a refusal.

rm -rf jobpack jobpack.tar.gz
echo secrets/dev.env >> allowlist.txt
python3 pack_job.py; echo exit:$?
Enter fullscreen mode Exit fullscreen mode

Verify: the process exits non-zero and prints FAIL: secret-shaped bytes. Then restore the allowlist. No "just this once."

cat > allowlist.txt << 'TXT'
src/README.md
TXT
rm -rf jobpack jobpack.tar.gz
python3 pack_job.py
Enter fullscreen mode Exit fullscreen mode

Verify again: grep -n dev.env jobpack/manifest.json; echo exit:$? must not find a path. exit:1 from grep is the success condition here. Awkward? Good. Make the happy path loud and the leak path louder.

What if your secret does not match the regex? Then it ships. That is a real limitation, not a footnote. Heuristic redaction is a seatbelt. It is not a vault.

Stage 5: Hand the pack to an untrusted runner

Now you have a tarball that contains a prompt file and a manifest. That is the only thing that should leave the laptop. Not your git history. Not secrets/. Not tmp/export.csv.

Unpack on the remote side, read jobpack/src/README.md, and produce one file: artifact/slugify.py. Nothing else. If the runner starts "cleaning up" unrelated files, you already lost the plot.

I keep the model and the sandbox on the untrusted side of this split. Disclosure: This article was prepared as part of MonkeyCode's product outreach. If you want that untrusted half off your laptop, MonkeyCode's free model access and free server option are one place to park the run. The packer and the contract test still stay local. That split is the point.

Whatever runner you use, bring back a single file plus a note of which manifest hash you packed. If the remote cannot tell you which manifest.json it saw, you cannot replay the job. Would you accept a CI run that cannot name its commit? Then do not accept an agent run that cannot name its pack.

Label this part as an unexecuted remote step if you have not wired a runner yet. The local gates do not depend on it.

# Proposed remote contract, not a vendor API:
# 1. tar tzf jobpack.tar.gz | grep -v '^jobpack/' && exit 1
# 2. sha256sum -c from manifest.json
# 3. write only artifact/slugify.py
Enter fullscreen mode Exit fullscreen mode

Verify locally before you send: shasum -a 256 jobpack.tar.gz and keep that digest in your notes. If the file you uploaded later has a different digest, stop. You packed twice. You do not know which pack the agent saw.

Stage 6: Contract-test the file you got back

Do not open the returned file in an editor and "see if it looks right." Looks-right is how invented helpers land in production. Write the test before you look.

Put a stub in place so the test harness exists even before the remote returns anything:

mkdir -p artifact tests
cat > tests/test_slugify.py << 'PY'
from __future__ import annotations

import importlib.util
from pathlib import Path

ART = Path(__file__).resolve().parents[1] / "artifact" / "slugify.py"


def load():
    if not ART.is_file():
        raise AssertionError("missing artifact/slugify.py")
    spec = importlib.util.spec_from_file_location("slugify", ART)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def test_contract():
    slugify = load().slugify
    assert slugify("") == ""
    assert slugify("Hello World") == "hello-world"
    assert slugify("  Hello---World!! ") == "hello-world"
    assert slugify("ABC") == "abc"
PY
Enter fullscreen mode Exit fullscreen mode

If you still have no artifact, plant a wrong one and confirm the test fails. Yes, fail it on purpose.

echo 'def slugify(text): return text' > artifact/slugify.py
python3 -m pytest -q tests/test_slugify.py; echo exit:$?
Enter fullscreen mode Exit fullscreen mode

Verify: non-zero exit. If pytest is missing: python3 -m pip install pytest. If the wrong stub passes, your contract is theater. Fix the assertions, not the mood.

When the remote file arrives, overwrite the stub and run the same command. Verify: python3 -m pytest -q tests/test_slugify.py exits 0. Then, and only then, copy artifact/slugify.py into a real tree.

A passing contract is not proof the code is kind. It is proof the code met the job you packed. That is the only promise this workflow makes.

Stage 7: Promote or burn

Keep promotion dumb.

  1. Contract tests pass.
  2. artifact/slugify.py contains no matches from secret_patterns.txt (scan the return path too; models echo context).
  3. git diff --no-index /dev/null artifact/slugify.py is a file you are willing to own.
  4. Commit from your identity, not from the runner's.

Scan the return path like this:

python3 - << 'PY'
import re, sys
from pathlib import Path
text = Path("artifact/slugify.py").read_text(encoding="utf-8")
patterns = [re.compile(p) for p in Path("secret_patterns.txt").read_text().splitlines() if p.strip() and not p.startswith("#")]
for pat in patterns:
    if pat.search(text):
        print(f"FAIL: artifact matched /{pat.pattern}/")
        sys.exit(1)
print("ok-stage-7-scan")
PY
Enter fullscreen mode Exit fullscreen mode

If the scan fails, delete the artifact. Do not "clean the key by hand and keep the rest." You do not know what else came back.

Decision table

Situation Ship the pack? Promote the artifact?
Allowlist empty No No
Secret regex hits a packed file No No
Tar digest != the digest you noted No No
Contract test fails Pack was fine No
Artifact matches a secret regex Pack was fine No
Tests pass, scan clean, diff is owned Yes Yes

Notice the last row is the only yes/yes. Everything else fails closed. That is intentional. Agents are cheap. Leaked staging keys are not.

Limitations

Regex redaction misses values that do not look like keys. A customer name in a fixture can still be sensitive. Binary files are out of scope for this packer. UTF-8 text only.

The contract test only encodes the examples you wrote. slugify("你好") is unspecified here. An agent can satisfy the test and still be wrong for your product. Write more cases or do not ship.

A free remote runner can still retain the pack. Redaction reduces blast radius. It does not give you a legal deletion guarantee. If the file cannot leave the building, it cannot leave the building. This workflow will not launder that rule.

I also did not pin tool schemas, wrap the shell, or hash-promote a full diff. Different jobs. This job is "stop uploading the tree."

Who should not use this

Skip it if your threat model is "no bytes leave this laptop, ever." A redacted pack is still a disclosure of the prompt and the fixtures.

Skip it if you cannot write a contract test for the artifact. Then you do not have a job. You have a vibe.

Skip it if the allowlist would be "the whole monorepo except node_modules." That is an ignore file wearing a hat. Split the task until the allowlist fits on one screen.

And skip remote runners entirely for production credentials, production data, or anything you would not paste into a ticket.

What to do Monday

Run stages 1–4 on /tmp/jobpack-demo. Plant the leak. Watch it fail. Then pack src/README.md only. That is the muscle memory.

The remote agent can wait. Your working tree does not need another frequent-flyer mile.

Top comments (0)