Skip to content
C.W.K.
Stream
Lesson 03 of 04 · published

The Agent Inherits Your Working Directory

~11 min · security, subprocess, agents, environment

Level 0Unsorted
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Nothing Pointed It There

A sampling session was found reading the source code of the very service that had invoked it. No path in the prompt, no configuration naming a repository, no tool designed to browse projects. It simply started in the directory the parent process happened to be running in — which was the service's own checkout — noticed there were files, and used them.

This is not misbehavior. Every subprocess inherits its parent's working directory; that is how processes work. What changed is the kind of child: a conventional subprocess does what it is told, while an agent with file access treats its surroundings as available context and explores them on its own initiative.

Inherited Environment Is a Broader Category

Once you see it for the working directory, the rest of the inherited environment looks different too. A child receives the parent's variables — including credentials for unrelated services — and its user identity, with whatever file access that identity carries. None of it was granted deliberately; all of it arrives because that is the default.

For an ordinary subprocess this is usually fine, because it only does what its arguments say. For an agent, the environment is not merely available, it is material. The correct mental model is not "a program I am running" but "a curious reader I am placing somewhere", and the question becomes: what is within reach of where I am putting it?

Half of It Is Fixed, and the Half That Is Not

The location half is one line: point the subprocess at a dedicated empty directory. That is what shipped, and it removes the entire class of accidental disclosure through inherited location. It cost one keyword argument and it addresses a failure mode no amount of prompt engineering could reach — because the prompt was never the problem. The agent was not told to read the repository; it was placed in one.

The environment half is also one line, and in this system it is not written. The call passes no env, so the child still inherits every variable the engine holds. That is worth stating plainly rather than quietly showing the version you wish you had shipped: an agent that can summarize is an agent that can repeat a credential it found in its environment, and the reason that has not bitten here is that nothing has asked it to — which is luck, not a control.

Where the Real Damage Would Be

In this instance the consequence was wasted tokens and a slightly confused sample. The reason it belongs in a track about untrusted input is what it composes with: an agent processing text from strangers, holding file-read tools, and sitting inside a directory full of source and configuration. Each of those is defensible alone. Together they are a path from a stranger's post to a description of your codebase, and the third one arrived entirely by accident.

An agent is placed somewhere, not merely executed. Everything within reach of that location is potential context, so choose the location deliberately — the default is wherever your service happens to live, which is the one place you would never have chosen.

Code

Placing the agent deliberately — and the half that was never written·python
# BEFORE: inherits the parent's cwd. Nothing pointed the agent at the
# repository -- it simply STARTED there, found files, and read them.
subprocess.run([GROK_BIN, "-p", prompt, "--yolo"], capture_output=True)


# AFTER, as actually shipped: a dedicated neutral directory, created
# once and reused. Not a temporary one -- a fixed empty dir under the
# engine's own data root. That is enough, because what matters is not
# that the directory is fresh but that nothing interesting is
# reachable from it.
def _run_grok(prompt: str) -> dict:
    workdir = DB_DIR / "grok-workdir"
    workdir.mkdir(parents=True, exist_ok=True)
    proc = subprocess.run(
        [GROK_BIN, "-p", prompt,
         "--disallowed-tools", DISALLOWED_TOOLS,
         "--yolo", "--output-format", "json",
         "--max-turns", str(SOCIAL_MAX_TURNS)],
        cwd=str(workdir),          # <- the LOCATION half, closed
        capture_output=True,
        text=True,
        timeout=SOCIAL_SAMPLE_TIMEOUT_SECONDS,
    )

# Note what is NOT in that call: there is no `env=`. The child
# therefore still inherits every variable this process holds. The
# location half of the problem is closed; the environment half is
# open. Both are one keyword argument. Only one of them got written.

External links

Exercise

Find every place your code spawns a subprocess that can read files, and check what working directory and environment it receives. For each, list what is reachable from that directory — source, configuration, credentials, other projects. Then decide which of those you would have granted on purpose.
Hint
The answer is almost always 'none of it', because nobody grants a working directory deliberately; it is inherited. The gap between what is reachable and what is intended is the entire finding, and it exists in essentially every codebase that shells out from a service directory.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.