Skip to content

29 medium working directory

Part V: Environment & State | Challenge §29

29. Working Directory Sensitivity

Severity: Medium | Frequency: Common | Detectability: Medium | Token Spend: Medium | Time: Low | Context: Low

The Problem

Many CLI tools resolve paths, find config files, or change behavior based on the current working directory. Agents set CWD at session start and may not realize that CWD matters for a given command — or that the correct CWD differs per command.

Implicit project root discovery:

$ cd /project/src/components && tool build
# Tool walks up to find package.json → /project/package.json
# Builds from /project, not /project/src/components
# Output paths are relative to /project
# Agent expected paths relative to /project/src/components

Config file discovered from CWD:

$ tool validate
# Looks for .toolrc in CWD, then parent dirs
# Agent's CWD=/tmp → no .toolrc found → uses global defaults
# Behavior differs from running in /project where .toolrc exists

Relative paths in output:

$ cd /project && tool list-files
{"files": ["src/index.ts", "src/utils.ts"]}
# Paths are relative to CWD at time of call
# Agent stores these, later calls tool from different CWD
# Paths are now wrong

cd side effects across tool calls:

# Agent calls: tool set-context --dir /project
# Tool internally does os.chdir("/project")
# Next tool call: CWD has changed, agent doesn't know

Impact

  • Different CWD → different config loaded → different behavior
  • Relative paths in output become stale or wrong
  • Tool finds wrong project root, operates on wrong codebase
  • Silent failures: command succeeds but affects wrong files

Solutions

Always output absolute paths:

{
  "files": [
    "/project/src/index.ts",
    "/project/src/utils.ts"
  ]
}

Include CWD used in meta:

{
  "meta": {
    "cwd": "/project",
    "project_root": "/project"
  }
}

Explicit --cwd / --root flag:

tool build --cwd /project
tool validate --root /project
# CWD-independent: agent always passes explicit path

Never mutate CWD of the calling process:

# Bad: os.chdir(target_dir)
# Good: use absolute paths internally; never change process CWD
import os
old_cwd = os.getcwd()
# operate with absolute paths throughout

For framework design: - All path outputs are absolute by default - meta.cwd included in every response - --cwd flag available on all commands as a framework standard - Framework never calls os.chdir() / process.chdir()

Evaluation

Score Condition
0 Path outputs are relative; no meta.cwd; tool behavior changes silently based on CWD
1 Some paths are absolute; meta.cwd absent; --cwd flag on some commands only
2 All path outputs are absolute; meta.cwd present in response; --cwd flag available
3 --cwd is a framework-level flag on all commands; framework never mutates process CWD; all paths absolute by default

Check: Run a command from two different directories and compare the path values in the output — any relative path that differs is a failure.


Agent Workaround

Always pass --cwd explicitly; verify meta.cwd in response matches intent:

import subprocess, json, os

project_root = "/absolute/path/to/project"

result = subprocess.run(
    ["tool", "build", "--cwd", project_root, "--output", "json"],
    capture_output=True, text=True,
    cwd=project_root,  # also set subprocess CWD as a belt-and-suspenders measure
)
parsed = json.loads(result.stdout)

# Verify the tool used the CWD we intended
meta_cwd = parsed.get("meta", {}).get("cwd")
if meta_cwd and os.path.realpath(meta_cwd) != os.path.realpath(project_root):
    raise RuntimeError(f"Tool ran from unexpected CWD: {meta_cwd}")

Convert relative paths in output to absolute before storing:

def resolve_paths(obj, base_dir: str):
    """Recursively resolve relative paths in output using meta.cwd as base."""
    if isinstance(obj, str) and (obj.startswith("./") or obj.startswith("../")):
        return os.path.normpath(os.path.join(base_dir, obj))
    if isinstance(obj, list):
        return [resolve_paths(i, base_dir) for i in obj]
    if isinstance(obj, dict):
        return {k: resolve_paths(v, base_dir) for k, v in obj.items()}
    return obj

cwd = parsed.get("meta", {}).get("cwd", os.getcwd())
data = resolve_paths(parsed.get("data", {}), cwd)

Limitation: If the tool outputs relative paths and provides no meta.cwd, the agent cannot safely resolve them — store the subprocess cwd at call time and use it as the base for all path resolution