DEV Community

Taylor Wang
Taylor Wang

Posted on

The Hash Matched for 48 Hours. cwd Was Never the Repo.

Have you ever proven a config file was correct, then watched the process ignore every change you just hashed? I did that across a 48-hour lab loop, and the logs kept loading defaults I had already deleted from the repo. The assistant kept confirming the file on disk, and I kept restarting a job that never opened that path. This field note is about cwd, relative opens, and a green check that meant almost nothing.

I am writing this as a reproducible walkthrough, not as a production war story with fake latency charts. Every command below is something you can run on a Linux box you already control. If a snippet is a proposed check rather than a captured transcript, I label it that way. Ready to print the path the kernel actually opened, not the path you meant?

Hour 0–8: I trusted the file I could see

The setup looked boring, which is how these loops usually start. A small Python worker read config.json from a relative path, then printed a startup banner and waited for work. I asked a coding assistant to keep the file valid, restart the worker, and stop when the banner showed the new env name. Have you noticed how confident a model sounds when sha256sum matches twice in a row?

I ran the loop with MonkeyCode's free model access and free server option, because I wanted the job living somewhere other than my laptop cwd. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I am not attaching model names, quotas, or hardware claims here; those details change, and they are not the bug. The useful part survives if you swap the host and the assistant for any remote shell plus any editor.

Proposed lab layout (create this on purpose):

~/lab-cwd-contract/
  worker.py
  config.json
  tests/test_path_contract.py
Enter fullscreen mode Exit fullscreen mode

config.json I used:

{
  "env": "lab-expected",
  "listen": "127.0.0.1",
  "note": "repo copy"
}
Enter fullscreen mode Exit fullscreen mode

worker.py I started from, on purpose, with a relative open:

import json
from pathlib import Path

# Proposed example: this is the bug, not a recommended loader.
cfg_path = Path("config.json")
print(f"cwd={Path.cwd()}")
print(f"opening={cfg_path.resolve()}")
with cfg_path.open() as handle:
    cfg = json.load(handle)
print(f"env={cfg['env']}")
Enter fullscreen mode Exit fullscreen mode

I hashed the repo file, saw the digest I expected, and told the assistant the config was good. Why would I print Path.cwd() when the file was sitting right there in ls?

Hour 8–24: every fix landed in the repo, none landed in the process

The banner still said env=lab-default, which was not even a key in the repo file. I blamed caching, then JSON trailing commas, then the assistant editing a buffer that never flushed. Does that spiral feel familiar, or is it only me who will rewrite a three-line loader twelve times before asking the kernel?

What I tried, in order:

  1. Re-hash ~/lab-cwd-contract/config.json after every patch.
  2. Add json.tool validation so a broken object could not silently load.
  3. Restart the worker from the assistant's shell snippet instead of my own.
  4. Duplicate the file to config.json.bak in case the process held an old descriptor.
  5. Ask the model to "make the path absolute," which it did by hard-coding /root/config.json.

That last patch compiled, started, and then failed closed because /root/config.json did not exist. The assistant treated the traceback as a missing file in the repo and created a second config at the hard-coded path. Now I had two files, one hash ritual, and still the wrong env. Have you watched a model solve the error it just created, while the original relative open stayed untouched in another copy of the worker?

Commands I should have run before hour 8, labeled here as the actual protocol:

# Proposed debug protocol — run these on the server that starts the worker.
pwd
readlink -f .
readlink -f config.json
sha256sum config.json ~/lab-cwd-contract/config.json
python3 -c "import os,json; print(os.getcwd()); print(json.load(open('config.json')))"
Enter fullscreen mode Exit fullscreen mode

On Linux, once the process exists, the kernel is more honest than the model:

# Replace PID with the worker you actually started.
ls -l /proc/$PID/cwd
ls -l /proc/$PID/fd | grep config
tr '\0' '\n' < /proc/$PID/environ | grep -E 'PWD|HOME|PYTHON'
Enter fullscreen mode Exit fullscreen mode

/proc/$PID/cwd was not the repo. It was the account home directory, which is where the free server session had been launched. A leftover config.json from an earlier experiment sat there with "env": "lab-default". The hash I kept repeating was a different inode. Want to guess how long I argued with the repo copy after that?

Hour 24–36: the contract I wish I had printed first

Relative paths are not "the file next to the script." They are "the file next to cwd," and cwd belongs to whoever spawned the process. __file__ belongs to the module. Those two facts are in the Python docs, and I still skipped them because the assistant could see the repo tree. Why do we keep asking models to confirm files they already have in context, instead of confirming the runtime view?

Here is the artifact I would drop in before any remote loop. It is small on purpose.

"""path_contract.py — fail closed if runtime paths drift from the repo."""
from __future__ import annotations

import json
import os
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent
EXPECTED_ENV = "lab-expected"


def runtime_snapshot() -> dict[str, str]:
    cwd = Path.cwd().resolve()
    cfg_from_cwd = (cwd / "config.json").resolve()
    cfg_from_file = (REPO_ROOT / "config.json").resolve()
    return {
        "cwd": str(cwd),
        "repo_root": str(REPO_ROOT),
        "config_via_cwd": str(cfg_from_cwd),
        "config_via_file": str(cfg_from_file),
        "same_inode": str(cfg_from_cwd == cfg_from_file),
    }


def load_repo_config() -> dict:
    path = REPO_ROOT / "config.json"
    with path.open() as handle:
        return json.load(handle)


def main() -> int:
    snap = runtime_snapshot()
    for key, value in snap.items():
        print(f"{key}={value}")
    if Path(snap["cwd"]) != Path(snap["repo_root"]):
        print("FAIL: cwd is not the repo root", file=sys.stderr)
        return 2
    if snap["same_inode"] != "True":
        print("FAIL: cwd config is not the repo config", file=sys.stderr)
        return 3
    cfg = load_repo_config()
    if cfg.get("env") != EXPECTED_ENV:
        print(f"FAIL: env={cfg.get('env')!r}", file=sys.stderr)
        return 4
    print("PASS: path contract holds")
    return 0


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

Proposed pytest that encodes the same rule, so a green suite cannot mean "zero assertions":

from pathlib import Path

import path_contract


def test_config_follows_file_not_cwd(tmp_path, monkeypatch):
    decoy = tmp_path / "config.json"
    decoy.write_text('{"env": "lab-default"}', encoding="utf-8")
    monkeypatch.chdir(tmp_path)
    snap = path_contract.runtime_snapshot()
    assert snap["config_via_file"] != snap["config_via_cwd"]
    cfg = path_contract.load_repo_config()
    assert cfg["env"] == "lab-expected"
Enter fullscreen mode Exit fullscreen mode

Run them like this, and notice the second command is the one that would have ended the loop:

cd ~
python3 ~/lab-cwd-contract/path_contract.py; echo exit:$?
cd ~/lab-cwd-contract
python3 path_contract.py; echo exit:$?
python3 -m pytest -q tests/test_path_contract.py
Enter fullscreen mode Exit fullscreen mode

Pytest still uses exit code 5 when it collected no tests, which is documented behavior, not folklore. My earlier "green" run was not pytest. It was a shell snippet that printed ok after starting the worker from $HOME. Have you noticed how many agent scripts treat any exit 0 as proof the system did what you meant?

Decision table I now keep next to the worker

Signal you see What it does not prove Check to run next
sha256sum config.json matches That this inode is the one open() will use readlink -f config.json and /proc/$PID/cwd
Model says the repo file was patched That the running process re-read anything Restart and print Path.cwd() inside the process
Worker banner printed That cwd equals repo root Compare __file__ parent with Path.cwd()
python worker.py returns 0 That tests ran, or that config loaded from git pytest exit code, plus load_repo_config()
Hard-coded /root/... stops a traceback That you found the real file Count how many config.json copies exist

If two rows fire at once, stop patching and print paths. Do not ask the model to "try another loader" until inode identity is boring and visible.

Hour 36–48: what broke, and what I would repeat

What broke was not JSON, and it was not the assistant's ability to edit a buffer. The job starter on the free server inherited a home-directory cwd, and my worker used a relative open, which is legal Python. The model did what models do: it optimized the files in its working tree, then treated hash equality as runtime equality. I helped by never printing /proc/$PID/cwd until I was already angry at the tool.

What I would repeat:

  • Print cwd, __file__, and resolve() on the first line of every remote worker.
  • Load config from Path(__file__).resolve().parent, then refuse to start if cwd differs and you did not opt in.
  • Hash both paths, or refuse to hash until readlink -f shows one inode.
  • Keep a decoy config.json in $HOME during labs, so a wrong open fails loudly instead of looking almost right.
  • Treat assistant-run shell snippets as untrusted cwd: cd to the repo in the same command string that starts the process.

Proposed one-liner I now paste before any remote start:

cd ~/lab-cwd-contract && python3 -c "from pathlib import Path; print(Path.cwd(), Path('worker.py').resolve())" && python3 path_contract.py && python3 worker.py
Enter fullscreen mode Exit fullscreen mode

Would I still use a free remote shell plus a free coding model for this kind of loop? Yes, because the mismatch shows up faster when the editor's tree and the process cwd are not the same laptop folder. The path contract is the method. The host is just where cwd gets a chance to lie.

Limitations, and who should not use this

This approach assumes a POSIX process with /proc, a project small enough to hold config next to __file__, and a worker you are allowed to restart. It does not fix container images that copy files after start, nor does it fix secret material you should never place on a shared free server. If your runtime chdirs on purpose, the "cwd must equal repo root" rule will fail closed, which is the point, but you will need an explicit allow-list instead of my return 2.

Do not use this as a substitute for real secret handling, production process managers, or multi-user hosts you do not control. Do not paste live credentials into any model prompt to "make the config work." The contract checks paths and a lab env string; it does not attest that the server is yours alone, or that a free tier will still be there tomorrow.

I also would not use relative open("config.json") in anything that systemd, cron, a panel button, or an assistant might launch. Those launchers almost never share the mental model of "I was in the repo in a previous message." If your team already injects config through the environment, keep doing that and still print cwd once, because the next relative path will be a log file.

What I want to hear back

If you already have a remote tab open, run the four-line protocol against a worker you trust and see whether cwd and __file__ agree. Did /proc/$PID/cwd point where you expected, or did you hash a perfectly correct file the process never opened?

Top comments (0)