Poetry -C changes working directory for subprocesses unexpectedly
poetry -C <subdir> run <cmd> runs <cmd> with the process working directory changed to <subdir>, not the shell's cwd. Any relative path the program resolves at runtime therefore points somewhere else.
The failure is a plain FileNotFoundError on a file that demonstrably exists where you are standing:
$ cd /repo && ls protected.yaml
protected.yaml
$ poetry -C fsrv run python -c "open('protected.yaml')"
FileNotFoundError: [Errno 2] No such file or directory: 'protected.yaml'This reads as a missing file, a bad mount, or a wrong cwd= on your own subprocess.run call, and sends you auditing all three. It is none of them. Reproduce the actual mechanism directly:
$ cd /repo && python3 -c "import os; print(os.getcwd())"
/repo
$ cd /repo && poetry -C fsrv run python -c "import os; print(os.getcwd())"
/repo/fsrvTwo things make this hard to see:
- You will not suspect Poetry.
-Cis understood as "which pyproject/virtualenv to use", and it does that correctly. Nothing in the symptom points at working directory. - The documented wording describes the wrong scope.
poetry help runsays of-C, --directory: "The working directory for the Poetry command (defaults to the current working directory). All command-line arguments will be resolved relative to the given directory." That describes argument resolution, so you conclude paths you never pass as arguments are unaffected. They are affected: the child process is genuinely chdir'ed, so a path hardcoded in the source (or built later from a config value, or a.envlookup, or a relativesqlite:///URL) resolves against<subdir>too.
High-blast-radius because monorepo conventions and AGENTS.md-style docs routinely prescribe poetry -C <pkg> run <tool> as the canonical invocation, so every relative path in every such command is quietly rebased, and it only bites the paths that happen to point outside the subdir.
Use -P/--project instead of -C/--directory. It selects the same project and virtualenv but leaves the working directory alone:
$ cd /repo && poetry -P fsrv run python -c "import os,sys; print(os.getcwd()); print(sys.prefix)"
/repo
/Users/…/virtualenvs/fsrv-6kYTy6Js-py3.12
$ cd /repo && poetry -P fsrv run python -c "open('protected.yaml'); print('opened OK')"
opened OKSame venv, cwd preserved, the failing script now works unchanged. Verified on Poetry 2.4.1.
The two flags are a deliberate, documented pair, and the distinguishing sentence is the last clause of each:
| Flag | Docs | Effect |
|---|---|---|
-C, --directory=DIR | "…All command-line arguments will be resolved relative to the given directory." | chdir to DIR |
-P, --project=DIR | "…All command-line arguments will be resolved relative to the current working directory." | cwd untouched |
Rules that follow:
- Default to
-Pforrun.-Cis the right choice only when you actually want the subdir as cwd (e.g. a build step whose relative outputs belong there). -Cis still correct for Poetry's own subcommands (install,lock,add), which operate on the project rather than executing your code, so the chdir is harmless or desirable.- Absolute paths sidestep the whole question and are the robust choice for anything a script opens itself, especially a repo-root file consumed from a package subdirectory.
- Do not chase this with
subprocess(cwd=…). Settingcwdon the parent has no effect: Poetry chdirs after you, so the child still lands in<subdir>. That misdiagnosis is the main time sink here. - Symptom-to-cause shortcut: a
FileNotFoundErroron a path that exists, under any wrapper CLI, is worth oneprint(os.getcwd())inside the child before anything else. Wrapper tools that resolve a project root are exactly the ones apt to move you.
General lesson beyond Poetry: when a launcher documents argument resolution, that phrasing does not bound the effect to arguments. Check whether the child's cwd moved, because a chdir silently rebases every relative path the program touches, not just the ones on the command line.