zed-industries/zed · error · io::Error
PermissionDenied
PermissionDenied
Error message
sandbox write grant {} is a symlink, not a directory What it means
Zed's sandbox component validates each configured write grant by canonicalizing it with an `O_PATH|O_NOFOLLOW` file descriptor and checking its file type via `fstat`. If the grant path's leaf resolves to a symlink (or, more generally, is not a directory), `from_canonical` returns `io::ErrorKind::PermissionDenied` with the message "sandbox write grant {path} is a symlink, not a directory". The check exists because `readlink` on an O_NOFOLLOW fd returns the symlink's own path, so the earlier path-equality comparison cannot catch a symlinked leaf.
Source
Thrown at crates/sandbox/src/util/canonical_path.rs:150
{
use std::os::unix::fs::OpenOptionsExt as _;
// `O_NOFOLLOW` makes a symlink *leaf* open the symlink itself
// (harmless with `O_PATH`) rather than its target, so we can detect
// and reject it below; intermediate components are still traversed
// and caught by the canonical-path comparison.
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_PATH | libc::O_CLOEXEC | libc::O_NOFOLLOW)
.open(&path)?;
let fd = OwnedFd::from(file);
// Reject a symlink leaf outright: a grant must name a real directory,
// and `readlink` of an `O_PATH|O_NOFOLLOW` fd on a symlink returns
// the symlink's *own* path (equal to `path`), so the comparison
// below wouldn't catch it.
let stat = nix::sys::stat::fstat(&fd).map_err(io::Error::from)?;
if stat.st_mode & libc::S_IFMT == libc::S_IFLNK {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"sandbox write grant {} is a symlink, not a directory",
path.display()
),
));
}
// Load-bearing: the pinned inode's real path must still be exactly
// the approved canonical path. If any component became a symlink
// after approval, the fd resolves elsewhere and this diverges.
let current = std::fs::read_link(format!("/proc/self/fd/{}", fd.as_raw_fd()))?;
if current != path {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"sandbox write grant {} was redirected to {}",
path.display(),View on GitHub (pinned to 5a9b9558db)
Solutions
- Resolve the symlink and pass the real directory: use the output of `readlink -f <path>` (or `realpath`) as the write grant instead of the symlink path.
- On macOS, replace `/tmp/...` grants with their `/private/tmp/...` real path.
- Restructure the environment so the sandboxed workspace is a real directory (move files rather than symlinking the whole project dir).
- If the symlink is intentional, grant the resolved parent (the symlink's target) and work inside it, not through the link.
Example fix
// before zed --sandbox-write-grant /tmp/project // after zed --sandbox-write-grant "$(readlink -f /tmp/project)"
Defensive patterns
Strategy: validation
Validate before calling
use std::path::Path;
fn validate_write_grant(path: &Path) -> std::io::Result<()> {
let meta = std::fs::metadata(path)?; // follows symlinks: stat the target
if !meta.is_dir() {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("{} is not a directory", path.display()),
));
}
if std::fs::symlink_metadata(path)?.file_type().is_symlink() {
let real = std::fs::canonicalize(path)?;
eprintln!("grant {} is a symlink; use {} instead", path.display(), real.display());
}
Ok(())
}
// call before handing the path to the sandbox: validate_write_grant(&grant)?; Type guard
fn is_real_directory(path: &std::path::Path) -> bool {
std::fs::symlink_metadata(path)
.map(|m| m.is_dir() && !m.file_type().is_symlink())
.unwrap_or(false)
} Try / catch
match launch_sandboxed(grant_path) {
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied
&& err.to_string().contains("is a symlink") =>
{
let real = std::fs::canonicalize(grant_path)?;
launch_sandboxed(&real)?
}
Err(err) => return Err(err.into()),
Ok(handle) => handle,
} Prevention
- Always pass the canonical (readlink -f / realpath) path as a sandbox write grant.
- On macOS, expand /tmp to /private/tmp before granting.
- Avoid symlinked project directories (dotfiles-managed stow links) inside sandboxed workspaces.
- Check symlink_metadata on grant paths in launch scripts before starting the editor.
When it happens
Trigger: Launching Zed (or a sandboxed subprocess) with a write grant (`--sandbox-write-grant` / sandbox dir policy) whose path is a symlink — e.g. pointing a grant at `/tmp/xyz -> /private/tmp/xyz` on macOS, or a symlinked dotdir like `~/projects -> /data/projects` passed directly as the grant.
Common situations: macOS `/tmp` -> `/private/tmp` indirection; symlinked home subdirectories (dotfiles managers stow-style links); CI containers where mounted paths are symlinked; passing `~` expansions that resolve through symlinks.
Related errors
- capability for process:exec {desired_command} {desired_args:
- failed to run `git init` in directory '{}'
- Could not detect a working directory in the container. Set E
- Could not detect a working directory; set EVAL_CLI_WORKDIR v
- OAuth endpoint must not point to private/reserved IP: {}
AI-assisted analysis of zed-industries/zed@5a9b9558db (2026-09-05).
Data as JSON: /api/errors/00dac5a283bd8e90.
Report an issue: GitHub.