# Avoid `File.cd!/2` in async tests

Do not use `File.cd!/2` to isolate filesystem work in concurrent tests. Its callback looks scoped, but the current working directory belongs to the entire BEAM.

```elixir
root = System.tmp_dir!() |> Path.join("file-cd-race")
dir_a = Path.join(root, "a")
dir_b = Path.join(root, "b")
File.mkdir_p!(dir_a)
File.mkdir_p!(dir_b)

original = File.cwd!()
parent = self()

task =
  Task.async(fn ->
    File.cd!(dir_a, fn ->
      send(parent, :inside_a)

      receive do
        :check -> File.cwd!() |> Path.basename()
      end
    end)
  end)

receive do
  :inside_a -> :ok
end

File.cd!(dir_b)
send(task.pid, :check)

IO.inspect(Task.await(task), label: "task inside A read")
# => task inside A read: "b"

File.cd!(original)
```

The task entered `dir_a`, but it read `dir_b` after another process changed the working directory. `File.cd!/2` restores the previous directory after its callback, even when the callback raises, but it cannot make a VM-global setting private to that process.

This is especially dangerous in `async: true` tests: relative reads, writes, and commands can occasionally run against another test's fixture directory. The failure depends on timing, so it often appears only in the full suite.

Keep filesystem paths absolute. When an external command needs a particular directory, use the `:cd` option of `System.cmd/3`; it sets the directory for that OS process without changing the BEAM's working directory.

```elixir
results =
  [dir_a, dir_b]
  |> Task.async_stream(fn dir ->
    System.cmd("pwd", [], cd: dir)
  end)
  |> Enum.map(fn {:ok, {output, 0}} ->
    output |> String.trim() |> Path.basename()
  end)

IO.inspect(results)
# => ["a", "b"]

File.rm_rf!(root)
```

The callback form remains useful in sequential scripts, but it is not an isolation boundary for concurrent code.

[`File.cd!/2` docs](https://hexdocs.pm/elixir/File.html#cd!/2)


---

Created by: almirsarajcic
Date: September 08, 2026
URL: https://elixirdrops.net/d/ZkUiYApD
