Have you ever watched a remote coding session pass every test while your laptop failed the same pytest command? I spent a weekend writing field notes on that split, because those two Python processes were never actually the same program. The remote workspace looked honest from the first green line, and my shell was quietly importing extra paths before any test file ran. This writeup is a debugging workflow I wish I had kept on a card, not a tour of a product or a claim about speed.
Why I even opened a free remote session
I had a flake that only showed up after I refactored a small command-line parser and its helper imports. Local pytest kept raising ModuleNotFoundError for a helper that the coding agent insisted it had just written into the tree. Was the agent inventing a file that never landed, or was I staring at the wrong interpreter again? I wanted a second machine that did not inherit a decade of shell profile habits, so I reached for a clean scratch box.
I used MonkeyCode because it currently offers free model access and a free server option, so a scratch workspace stayed cheap enough to throw away.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
I am writing as a user of that scratch box, and I am not reporting quotas, model names, hardware, or any benchmark I did not take. Those availability details change often, and inventing extra numbers would make these notes age badly by next week. The useful part is the split between a clean remote interpreter and a laptop that still trusts your dotfiles.
Hour 0–8: I trusted a green remote pytest
The first remote session looked perfect from the log, which is exactly how this kind of weekend gets wasted. The agent wrote a helper module, added one import, and ran a short pytest command on the remote workspace. The output was a calm green line, so I copied the same command onto my laptop and watched it explode. Here is the command both sides ran, with no extra flags and no attempt to isolate the interpreter:
python -m pytest tests/test_cli_parse.py -q
Locally I got a failure that looked like a missing project file, which sent me down the wrong grep path for hours.
ModuleNotFoundError: No module named 'cli_helpers'
So I did what I always do when an agent and a traceback disagree: I grepped the repo, then I blamed collection order. I even blamed a missing __init__.py, which was sitting in the package directory the entire time I was arguing with pytest. What actually broke was simpler than any of those theories, and it had nothing to do with the helper the agent had written. The remote server started from a clean interpreter, while my laptop started from a login shell that still exported old PYTHONPATH fragments.
Hour 8–24: I printed the wrong Python
I thought which python would settle the argument, because both machines answered with a path that looked like a virtualenv. The directory names matched closely enough that I stopped looking, and that laziness cost me the rest of the afternoon. The module graphs did not match at all, which is a different bug from "pytest cannot see my file." I started collecting an environment fingerprint instead of arguing with the traceback, and that is the artifact I would repeat.
The fingerprint script
Save this as scripts/env_fingerprint.py and keep it in stdlib only, so both the laptop and the scratch server can run it. It does not call a vendor API, and it does not pretend to be a complete security audit of sys.path.
#!/usr/bin/env python3
"""Print a stable-enough fingerprint of the interpreter that will run tests."""
from __future__ import annotations
import hashlib
import json
import os
import site
import sys
from pathlib import Path
def _hash_file(path: Path) -> str | None:
if not path.is_file():
return None
digest = hashlib.sha256()
digest.update(path.read_bytes())
return digest.hexdigest()[:12]
def fingerprint() -> dict:
user_site = site.getusersitepackages()
sitecustomize_paths = []
for entry in [Path(user_site), *map(Path, sys.path)]:
candidate = entry / "sitecustomize.py"
if candidate.is_file():
sitecustomize_paths.append(
{
"path": str(candidate),
"sha256_12": _hash_file(candidate),
}
)
return {
"executable": sys.executable,
"version": sys.version.split()[0],
"prefix": sys.prefix,
"base_prefix": sys.base_prefix,
"in_venv": sys.prefix != sys.base_prefix,
"cwd": os.getcwd(),
"pythonpath": os.environ.get("PYTHONPATH", ""),
"user_site_enabled": site.ENABLE_USER_SITE,
"user_site": user_site,
"sitecustomize_files": sitecustomize_paths,
"sys_path_head": sys.path[:8],
}
if __name__ == "__main__":
print(json.dumps(fingerprint(), indent=2))
Run it in both places with the same invocation style you already use for tests, not with a different alias from your prompt.
python scripts/env_fingerprint.py > /tmp/fp-local.json
# later, on the remote workspace
python scripts/env_fingerprint.py > /tmp/fp-remote.json
Then compare the two JSON files with a tiny reader that only prints keys you actually care about:
python - <<'PY'
import json
from pathlib import Path
local = json.loads(Path("/tmp/fp-local.json").read_text())
remote = json.loads(Path("/tmp/fp-remote.json").read_text())
keys = [
"executable", "version", "in_venv", "cwd",
"pythonpath", "user_site_enabled", "sitecustomize_files",
]
for key in keys:
if local.get(key) != remote.get(key):
print(f"DIFF {key}")
print(f" local : {local.get(key)!r}")
print(f" remote: {remote.get(key)!r}")
PY
On my laptop the interesting DIFF was not the version string; it was sitecustomize_files plus a stale PYTHONPATH entry. That extra path pointed at an old checkout of helper utilities I had forgotten about during a previous experiment. The remote fingerprint had an empty pythonpath field and no sitecustomize.py file, so the agent-created module won the import. Would you have grepped for cli_helpers inside the repo first, or would you have grepped your home directory like a pessimist?
I grepped the repo, which was the wrong tree again, just wearing a different costume than last time.
The ghost module
The local failure was not a missing file in the project, even though the exception text made that story feel complete. A user-level sitecustomize.py imported a cli_helpers name from a completely different folder, then failed when that folder lagged the refactor. The remote server never loaded that file, so the module the agent created looked like the only possible import. Confirm the ghost with commands that print files, not names, because names are cheap and paths are evidence:
python -c "import sitecustomize; print(getattr(sitecustomize, '__file__', 'no file'))"
python -c "import cli_helpers; print(cli_helpers.__file__)"
python -m site
env | grep -E '^(PYTHONPATH|PYTHONNOUSERSITE|VIRTUAL_ENV)='
If cli_helpers.__file__ lives outside your repository, stop treating the traceback as a project bug and start treating your login environment as hostile. A clean remote pytest run cannot see a file that only exists because your laptop imported it first.
Hour 24–48: I made the two boxes disagree on purpose
Once I could see the split, I wanted a rule I could reuse the next time a remote session looked greener than my laptop. I did not want another night of copying remote output onto a dirty shell, because local is still the interpreter I ship from. The point of the scratch box is a second opinion, not a second reality. So I wrote the comparison down as a table I could follow when I was tired.
Decision table I actually used
| Signal | Trust the remote green run? | What to do next |
|---|---|---|
Fingerprints match on version, venv, cwd, PYTHONPATH, and sitecustomize
|
Yes, as a second opinion | Copy the patch, then rerun the same pytest locally |
Remote is clean, local has PYTHONPATH or user-site files |
No | Isolate local with PYTHONNOUSERSITE=1 and an empty PYTHONPATH, then retest |
| Remote Python version does not match local | No | Recreate the remote venv from the same lockfile, or stop comparing |
| Agent created a file the fingerprint cwd cannot see | No | Print os.getcwd() from the test process, not from your prompt |
Tests pass only when the agent shells out without -I
|
No | Rerun with python -I -m pytest so user site and PYTHONPATH are ignored |
Isolation commands that made the laptop honest
The commands look noisy in a terminal history, but they removed my home directory from the import story:
env -u PYTHONPATH PYTHONNOUSERSITE=1 python -I -m pytest tests/test_cli_parse.py -q
Isolated mode ignores PYTHONPATH, user site packages, and several current-directory import tricks that make agent sessions look smarter than they are. If your suite only passes without -I, the suite is not pinned to the project; it is borrowing your home directory. I also started launching agent-suggested commands with an explicit interpreter path, because python in a prompt is a rumor.
"$PWD/.venv/bin/python" -I scripts/env_fingerprint.py
"$PWD/.venv/bin/python" -I -m pytest tests/test_cli_parse.py -q
That looks fussy on a sticky note, and it still stopped me from celebrating a remote green bar my shipping interpreter could not reproduce. Do you want the agent to optimize for a clean room, or for the process that will run after you close the laptop lid?
A tiny test plan before I trust a patch
I now refuse to accept an agent patch until these four checks produce the same story on both machines:
-
python -c "import sys; print(sys.executable, sys.version.split()[0])"matches the lockfile era you think you are on. -
scripts/env_fingerprint.pyshows no unexplainedDIFFinpythonpathorsitecustomize_files. -
python -I -m pytest tests/test_cli_parse.py -qpasses locally, not only in the remote log. -
python -c "import cli_helpers; print(cli_helpers.__file__)"prints a path inside the repo you are about to commit.
If step 4 points at $HOME, the patch is theater. If step 3 fails only locally, the remote green run is a clean-room illusion. If step 2 is noisy and you cannot explain every DIFF in one sentence, you are not done.
What I would repeat
These are the notes I would keep, because they survived contact with a second machine.
- Fingerprint first, argue second. Dump
scripts/env_fingerprint.pyfrom every machine that claims a test result. - Treat a free remote server as a clean room, not as production, because clean rooms hide user-site bugs.
- When an agent writes a module, print
__file__for the import you care about instead of trusting the name. - Re-run the failing test under
python -Ibefore you rewrite application code or add another__init__.py. - If you use a coding agent with free model access, ask it to patch against the fingerprint diff, not against the first traceback.
The last item is where free model access actually helped, because I could paste two JSON fingerprints instead of a naked traceback. I asked for a patch that made local isolated pytest pass, rather than asking anyone to "fix ModuleNotFoundError" in the abstract. Vague prompts chase ghosts across two filesystems, while fingerprints give the model a constraint it can fail against. If you already needed a scratch box, the free server option is enough for this clean-room check, not as a substitute for CI.
What broke, in plain language
- The remote workspace did not load my user
sitecustomize.py, so it could not show the real import order. -
which pythonmatched closely enough that I stopped looking, which was lazy rather than scientific. - Pytest collection order made the failure look like a missing project file instead of a haunted user site.
- I nearly added a useless
__init__.pyand a second copy ofcli_helpersbecause the agent was optimizing for the clean server.
None of that is exotic if you have been writing Python long enough to have a messy $HOME. It is just easy to miss when you bounce between a laptop and a scratch box and both of them answer to the same command name.
Limitations, and who should skip this
This workflow is for people who run Python tests in more than one environment and let a coding agent propose patches. It is not a substitute for a lockfile, a container, or a CI job with a pinned image. A fingerprint script explains disagreement; it does not create a reproducible release.
Do not use this approach if you need cryptographic provenance of the remote host, because a free server option is still someone else's machine. Do not use it if your tests require GPU drivers, private package indexes, or secrets that should never leave your laptop. Do not treat a green remote run as a performance number, because I did not benchmark anything and you should not either.
The fingerprint script hashes sitecustomize.py only, which is a deliberate narrow cut. It will not catch a .pth file that injects a path, and it will not catch a conda activation hook that mutates sys.path after the process starts. If you live in conda, add CONDA_PREFIX and the tail of sys.path to the JSON before you trust a comparison. Isolated mode will also break legitimate user-site workflows, so python -I can look like a regression when your team installs shared debugging helpers with pip install --user.
Field notes I am keeping
I still like a clean remote session when I want a second pair of eyes on a patch, especially when my dotfiles are untrustworthy. I just refuse to merge from that session until the fingerprint diff is empty, or until I can explain every remaining DIFF in one sentence. If I cannot explain a remaining DIFF in plain language, I do not ship the patch, even when the remote log looks calm.
If you try the fingerprint script, run it twice: once from your prompt and once from the exact command the agent used. Those two runs disagree more often than I want to admit, which is the whole reason I wrote the notes down.
Top comments (0)