Say you have a Rust workspace with turbo run lint wired to cargo clippy. It passes, and the result is cached. Then you add a clippy.toml with a stricter threshold and run lint again. Turbo prints FULL TURBO. No warnings, no errors, a green run.
The lint should have failed. Clippy reads that file, turbo's hash didn't, so turbo replayed an answer that was no longer true.
That is the whole problem with caching tasks for tools like Cargo, Go and uv, which Turborepo 2.11 added as experimental. Turbo hashes a task's inputs and replays the old output when the hash matches. So the hash has to cover everything the tool reads, and for a linter or a compiler that is a harder question than it sounds.
Two ways to be wrong
The rule is that the hash should change exactly when the tool's result could change. It can break in either direction.
The two are not equally bad. A stale hit hands you a wrong answer that looks fine, and a miss just costs time. So when you can't tell whether something matters, you include it. But you can't include everything, or the hash moves on every install and the cache never hits.
Config in the directories between
Linters and test runners don't only look for config at the repo root. They start from the file they are checking and walk up until they find something: clippy for clippy.toml, ruff for the closest ruff.toml or pyproject.toml, pytest for its ini file and then every conftest.py down from its root.
Turbo hashed the root and the package, nothing in between. So a packages/ruff.toml that turned a clean lint into ten errors left the hash alone. The fix is to add each tool's config filenames as input globs for every directory strictly between the package and the root: ancestor_config_globs in Cargo, ten filenames per intermediate directory in uv.
What the command touches decides which directories count. cargo clippy --package app also lints the local crates app depends on, so their ancestors are hashed. cargo fmt --package app only formats app, so theirs are not. The uv globs are deliberately too broad: a tox.ini with no pytest section still changes the hash, because deciding whether it matters means parsing it the way pytest does. The worst case is a miss, which is the cheaper mistake.
Some config can't be seen at all. Clippy and rustfmt keep searching above the repository root, and rustfmt also reads a file in your home directory. A repository hash has no way to cover those.
The same path, spelled twice
Now the other direction. Go finds its working directory with os.Getwd, which trusts the $PWD environment variable if it names the current directory, and otherwise returns the path with symlinks resolved. Turbo was passing along whatever $PWD it inherited.
Turbo compares the package directories go list reports with the module directory it computed. When the spellings differed, the comparison failed and discovery fell back to a conservative default. From a subdirectory, an executable module lost its cached go build . and ran an uncached go build ./....
The fix is one line in run_go: set PWD to the directory the command runs in. A guard next to it keeps the conservative fallback for a workspace member reached through an in-repo symlink, since hashing does not follow those. The obvious alternative was to canonicalize the repository root everywhere turbo builds a path, but that would change hashes and displayed paths for every symlinked checkout and every toolchain. Setting the variable where the process is spawned touches only the process that was wrong.
The general point is that anything that differs between machines or invocations and reaches the key makes it unstable, even through a side door like an inherited environment variable.
Binary, according to whom
With * text=auto in .gitattributes, git converts CRLF to LF for files it judges to be text and leaves binary files alone. Turbo mirrors this when hashing, so its hash of a file matches the blob git would store. But the mirror was approximate.
Where the rules disagree, both failures show up (#14697). The fix ports git's classification into a byte-class table folded into the existing CRLF scan. A naive version made text=auto hashing about 2.7 times slower; counting only the non-printable classes and stopping at the first NUL brought text throughput to 0.82 GB/s against 1.10 GB/s before, and binary content back to baseline. To check the port, hash generated files with git hash-object --path and with turbo, and compare. Git is the reference.
One gap remains, as a tradeoff. Git skips conversion for a path whose indexed blob already contains CRLF, which happens when files were committed before the attribute was added. Handling that needs the index blob, which the hashing path doesn't have, so the code documents it.