vercel/turborepo · error · LocalTurboProcess
Failed to execute local `turbo` process.
Error message
Failed to execute local `turbo` process.
What it means
The global turbo shim resolved the repo-local turbo binary and tried to spawn it via runtime.child_spawner.spawn(command) (run.rs:524 region); the OS-level spawn failed and is wrapped as Error::LocalTurboProcess with the io::Error preserved as #[source] (run.rs:102-103). The message is intentionally generic — the real cause (ENOENT, EACCES, EMFILE, missing cwd…) is in the chained source error.
Source
Thrown at crates/turborepo-shim/src/run.rs:524
raw_args.append(&mut shim_args.forwarded_args);
raw_args
}
fn spawn_child_turbo<R, C, S, V>(
runtime: &ShimRuntime<R, C, S, V>,
command: process::Command,
err: fn(std::io::Error) -> Error,
) -> ShimResult<R::Error>
where
R: TurboRunner,
C: ConfigProvider,
S: ChildSpawner,
V: VersionProvider,
{
let child: Arc<SharedChild> = match runtime.child_spawner.spawn(command) {
Ok(child) => child,
Err(e) => return ShimResult::ShimError(err(e)),
};
// The child turbo process shares our process group and will receive
// SIGINT directly from the kernel when the user presses Ctrl+C.
// Ignore SIGINT in the shim *after* spawning so the child inherits
// the default disposition (and can register its own handler), while
// the shim stays alive to collect the child's exit status.
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGINT, libc::SIG_IGN);
}
let exit_status = match child.wait() {
Ok(status) => status,
Err(e) => return ShimResult::ShimError(err(e)),
};
let exit_code = exit_status.code().unwrap_or_else(|| {
debug!("child turbo failed to report exit code");View on GitHub (pinned to f9245100cf)
Solutions
- Reinstall dependencies (`pnpm install` / `npm install`) to regenerate node_modules/.bin/turbo
- Inspect the source error: run `ls -l node_modules/.bin/turbo*` and try executing it directly to see the errno
- If EACCES on Linux/WSL: `chmod +x` the binary, or mount the repo with metadata enabled
- Install turbo as a direct devDependency so the local binary always exists
Example fix
# before ls node_modules/.bin/turbo # missing or not executable # after pnpm install chmod +x node_modules/.bin/turbo # if needed (WSL/DrvFs) node_modules/.bin/turbo --version
Defensive patterns
Strategy: try-catch
Validate before calling
// pre-flight the local binary before handing off to the shim
let bin = repo.join("node_modules/.bin/turbo");
#[cfg(unix)]
{ use std::os::unix::fs::PermissionsExt; assert!(fs::metadata(&bin)?.permissions().mode() & 0o111 != 0); } Type guard
fn spawnable(p: &Path) -> bool { p.is_file() && fs::metadata(p).map(|m| m.permissions().readonly() == false).unwrap_or(false) } Try / catch
// the io::Error source carries the errno — branch on it
match run() {
Err(Error::LocalTurboProcess(source)) => match source.kind() {
std::io::ErrorKind::NotFound => reinstall_deps(),
std::io::ErrorKind::PermissionDenied => fix_exec_bit(),
_ => return Err(source.into()),
},
r => r?,
} Prevention
- Pin turbo as a repo devDependency so the local binary always exists
- After partial installs or file copies, re-run the package manager before turbo
- Keep the exec bit when moving repos across filesystems (WSL DrvFs)
When it happens
Trigger: Spawning node_modules/.bin/turbo (or the resolved local turbo path) fails: the binary vanished between detection and spawn (ENOENT), is not executable / has a broken shebang (EACCES/ENOEXEC), or the process hit fd/memory limits (EMFILE/ENOMEM).
Common situations: Interrupted or partially-applied package-manager install leaving a broken .bin entry Repo copied from Windows/another machine losing the exec bit (WSL DrvFs, chmod issues) Corrupt npx/pnpm-managed shims after a Node version switch
Related errors
- Failed to execute `turbo` via `npx`.
- `git ${args[0]}` failed: ${formatError(error)}
- no threads found for process {process_id}
- {message}
- {message}
AI-assisted analysis of vercel/turborepo@f9245100cf (2026-08-17).
Data as JSON: /api/errors/697a954db4f48711.
Report an issue: GitHub.