DEV Community

Taylor Wang
Taylor Wang

Posted on

The Script Wrote ./results.json. I Spent 48 Hours Opening the Wrong Tree.

Have you ever watched a generated batch script print success, then spent two nights opening directories that never contained the file? I did that on this pass, and the notes still feel slightly rude to my past self. This is a 48-hour field log about relative paths, swallowed exit codes, and a free remote shell that never shared my laptop’s working directory. If you drop agent-written jobs onto a machine you do not fully own, you will meet this disagreement sooner than you want.

I was not chasing a clever algorithm. I wanted a receipt I could grep: one JSON blob, one markdown line, and a command I could rerun without folklore. The interesting failure was social, in a way. The process told me it wrote results.json, and I believed the string more than I believed pwd.

Hour 0–8: I asked for a small job, not a philosophy

Why scaffold a platform when I only needed a counter with a paper trail? I wrote a short contract in comments, then let a free coding model fill the glue because the glue is where I waste pride. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode’s free model access was enough to draft the first script, and the free server option was the second machine that actually ran it. I will not name models, quote quotas, or invent hardware details, because none of those were the bug. The bug was that the script and I disagreed about what . meant once a wrapper changed directory.

The first generated shape looked like this, labeled as the draft I started from and then trimmed:

# processor.py — initial draft, relative outputs on purpose
from pathlib import Path
import json

def run(src: str = "fixtures/events.jsonl") -> None:
    path = Path(src)
    rows = path.read_text().splitlines()
    out = {"count": len(rows), "source": src}
    Path("results.json").write_text(json.dumps(out, indent=2))
    Path("reports").mkdir(exist_ok=True)
    Path("reports/summary.md").write_text(f"# count={out['count']}\n")
    print("wrote results.json")

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

Looks harmless, right? Relative paths are how every tutorial starts, and that print statement feels like a contract you could take to court. I copied the tree, ran the module, and treated stdout as a filesystem.

Hour 8–24: success was a string, not a file

Did I print pwd before the first server run? Of course not. I trusted the repo layout I could see in the editor, which is a very laptop kind of faith. The free server session did not open where my editor root lived, and the wrapper I keep for “just run this” made it worse.

pwd
ls -la
python processor.py
# wrote results.json
ls -la results.json reports/summary.md
Enter fullscreen mode Exit fullscreen mode

On the laptop those last paths resolved. On the server the listing was empty, so I assumed a sync bug and reran the copy. I reran the script. I opened results.json in the editor and wondered why the timestamp never moved. Have you noticed how long you can debug a file that is not the file the process used?

This was the command that ended the superstition:

python -c "import os, pathlib; print(os.getcwd()); print(pathlib.Path('.').resolve())"
find "$HOME" -name 'results.json' 2>/dev/null
Enter fullscreen mode Exit fullscreen mode

The file existed. It just did not exist in the repository I was staring at. The wrapper had changed into a tools directory, then invoked python /path/to/processor.py, and every relative write followed process cwd rather than the script file. I also found a second copy under a leftover extract directory. Two successes, two trees, zero agreement.

What actually broke

  • Relative Path("results.json") follows os.getcwd(), not Path(__file__).parent.
  • A convenience wrapper changed directory before exec, and I never printed that directory.
  • The free server home was not my laptop home, so muscle-memory paths were confident lies.
  • print("wrote results.json") does not prove the write landed where a human will look.
  • find on the repo root cannot see a file that was written beside the wrapper.

I kept asking the editor to refresh a path the process had never opened. That is not a networking mystery. That is cwd.

Hour 24–36: the wrapper ate the only honest signal

While hunting files I “fixed” install steps by stuffing them into a shell wrapper, which is the kind of thing you write after midnight and defend until breakfast. The wrapper looked busy, so I trusted it. Busy is not the same as strict.

#!/usr/bin/env bash
# run.sh — the version I should not have trusted
cd "$(dirname "$0")/../tools" || true
python -m pip install -r requirements.txt
python /opt/job/processor.py
echo "job finished"
Enter fullscreen mode Exit fullscreen mode

See the missing set -e? See || true? See the unconditional job finished? Pip failed on the server because a build dependency was absent, and the script still imported a stdlib-only fallback I had left in during drafting. Later checks looked green because they imported that fallback, not the package I believed I had installed. The job was not frozen. It was politely lying.

I replaced the compliment machine with a tiny receipt helper. Treat this as a local contract, not a product claim.

# run_receipt.py
"""Record cwd, real paths, and child exit codes. Local helper, not a platform."""
from __future__ import annotations

import json
import os
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path

def main(argv: list[str]) -> int:
    if len(argv) < 2:
        print("usage: run_receipt.py <output-dir> <cmd> [args...]", file=sys.stderr)
        return 2

    out_dir = Path(argv[0]).resolve()
    cmd = argv[1:]
    out_dir.mkdir(parents=True, exist_ok=True)

    receipt = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "cwd": os.getcwd(),
        "cwd_resolved": str(Path(".").resolve()),
        "argv": cmd,
        "pid": os.getpid(),
    }

    proc = subprocess.run(cmd, text=True)
    receipt["returncode"] = proc.returncode

    claimed = ["results.json", "reports/summary.md"]
    receipt["artifacts"] = []
    for rel in claimed:
        p = Path(rel)
        receipt["artifacts"].append(
            {
                "claimed": rel,
                "exists": p.exists(),
                "resolved": str(p.resolve()) if p.exists() else None,
            }
        )

    receipt_path = out_dir / "receipt.json"
    receipt_path.write_text(json.dumps(receipt, indent=2))
    print(f"receipt={receipt_path}")
    return proc.returncode

if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
Enter fullscreen mode Exit fullscreen mode

Run it from the directory you actually intend, including a deliberately wrong directory once:

cd /opt/job
python run_receipt.py /opt/job/receipts python processor.py
cat /opt/job/receipts/receipt.json

# wrong-cwd probe — this is the test that would have saved the first night
cd /tmp
python /opt/job/run_receipt.py /tmp/receipts python /opt/job/processor.py
cat /tmp/receipts/receipt.json
Enter fullscreen mode Exit fullscreen mode

If returncode is nonzero, the wrapper stays nonzero. If exists is false, you stop arguing with the editor and start arguing with cwd. Would I go back to echo job finished? Not unless I want another unbillable night.

A tiny test I will keep

This is a reproducible check, not a benchmark, and it does not pretend to time anything. It fails in a useful way when cwd and the script directory drift.

# test_cwd_contract.py
from pathlib import Path
import os
import subprocess
import sys

def test_processor_writes_beside_cwd_not_script(tmp_path: Path) -> None:
    repo = tmp_path / "repo"
    other = tmp_path / "other"
    repo.mkdir()
    other.mkdir()
    script = repo / "processor.py"
    script.write_text(Path("processor.py").read_text())
    (repo / "fixtures").mkdir()
    (repo / "fixtures" / "events.jsonl").write_text("a\nb\n")

    proc = subprocess.run(
        [sys.executable, str(script)],
        cwd=other,
        env=os.environ.copy(),
        text=True,
        capture_output=True,
    )
    assert proc.returncode == 0
    # Document the trap: the write followed cwd, not the script.
    assert (other / "results.json").exists()
    assert not (repo / "results.json").exists()
Enter fullscreen mode Exit fullscreen mode

That assertion is the whole lesson. If you want artifacts beside the script, you must say so in code instead of hoping the shell was polite:

ROOT = Path(__file__).resolve().parent
(ROOT / "results.json").write_text(json.dumps(out, indent=2))
Enter fullscreen mode Exit fullscreen mode

Better still, take an absolute output directory from the environment so cron, ssh, and an editor root cannot silently disagree:

import os
from pathlib import Path

OUTPUT_DIR = Path(os.environ.get("OUTPUT_DIR", Path(__file__).resolve().parent)).resolve()
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
(OUTPUT_DIR / "results.json").write_text(json.dumps(out, indent=2))
Enter fullscreen mode Exit fullscreen mode

Decision table I wish I had on hour one

Situation Use relative . Use Path(__file__).parent Use an env absolute path
Interactive REPL already in the repo Yes Optional No
Wrapper, systemd, or cron may cd No Yes Yes, if ops owns the tree
Free or ephemeral server home No Yes Yes, and log resolve()
Model-generated scaffold Assume no Default to this Pass OUTPUT_DIR
Two copies of the same job on disk No Yes Yes, include a run id

I now treat “the model said it wrote a file” as a hypothesis. Hypotheses get a receipt. Feelings do not.

Hour 36–48: what I would repeat

Would I still use a free model to draft glue? Yes, because the glue is not where I want to spend pride, and a second pass on __file__ is cheaper than another hunt. Would I paste the first draft onto a free server and trust stdout? Not again. The server earned its keep as a second computer with a different cwd, a thinner PATH, and a home directory that is not mine. That difference is the test, not an inconvenience to click through.

Repeatable loop I am keeping:

  1. Freeze the output contract: absolute directory via OUTPUT_DIR or __file__, written down before generation.
  2. Generate or edit the script, then run run_receipt.py from a cwd that is wrong on purpose.
  3. Open receipt.json before opening the artifact; if exists is false, do not debug the algorithm.
  4. Fail the wrapper on any nonzero child, including pip, and delete || true from install steps.
  5. Copy the same commands onto the free server and compare receipts, not memories of what the laptop did.

I also started printing three lines at the top of every job, because future-me will forget this log:

import os, sys
from pathlib import Path
print(f"argv={sys.argv}", flush=True)
print(f"cwd={os.getcwd()}", flush=True)
print(f"script={Path(__file__).resolve()}", flush=True)
Enter fullscreen mode Exit fullscreen mode

If those three lines disagree with the directory in your editor, you are already debugging the wrong tree. Stop. Do not reread the parser. Do not blame the model’s JSON. Fix the contract.

Limitations, because this is not a platform story

Do not use this wrapper as a production supervisor. It does not rotate logs, it does not lock files, and it does not survive a host that wipes the disk between sessions. Free model drafts plus a free server shell are fine for learning a path contract; they are the wrong place to store secrets, customer data, or anything you cannot reconstruct from git. If your job needs a stable hostname, a guaranteed volume, or a long-lived environment you can name in inventory, stop here and use a machine you can actually describe.

I also would not feed a model a prompt that says “just make it work” without naming the output directory in the same breath. Agents assume . means the repo. Processes disagree, wrappers disagree harder, and free servers are honest about neither until you print resolve(). If you cannot afford a wrong-cwd test, you cannot afford a generated script.

Who should skip this approach? Anyone shipping a stateful worker, anyone without a fixture they can rebuild, and anyone who needs the job to outlive the home directory. This loop is for catching a lying success message before you spend a second night on the wrong tree.

If you want to try the same receipt loop with a free-model draft and a free server shell, that pairing is what I used here; keep the receipt file even if you never touch the product.

Top comments (0)