{"record":{"id":"00dac5a283bd8e90","repo":"zed-industries/zed","slug":"permissiondenied","errorCode":"PermissionDenied","errorMessage":"sandbox write grant {} is a symlink, not a directory","messagePattern":"sandbox write grant (.+?) is a symlink, not a directory","errorType":"error_code","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/sandbox/src/util/canonical_path.rs","lineNumber":150,"sourceCode":"        {\n            use std::os::unix::fs::OpenOptionsExt as _;\n            // `O_NOFOLLOW` makes a symlink *leaf* open the symlink itself\n            // (harmless with `O_PATH`) rather than its target, so we can detect\n            // and reject it below; intermediate components are still traversed\n            // and caught by the canonical-path comparison.\n            let file = std::fs::OpenOptions::new()\n                .read(true)\n                .custom_flags(libc::O_PATH | libc::O_CLOEXEC | libc::O_NOFOLLOW)\n                .open(&path)?;\n            let fd = OwnedFd::from(file);\n\n            // Reject a symlink leaf outright: a grant must name a real directory,\n            // and `readlink` of an `O_PATH|O_NOFOLLOW` fd on a symlink returns\n            // the symlink's *own* path (equal to `path`), so the comparison\n            // below wouldn't catch it.\n            let stat = nix::sys::stat::fstat(&fd).map_err(io::Error::from)?;\n            if stat.st_mode & libc::S_IFMT == libc::S_IFLNK {\n                return Err(io::Error::new(\n                    io::ErrorKind::PermissionDenied,\n                    format!(\n                        \"sandbox write grant {} is a symlink, not a directory\",\n                        path.display()\n                    ),\n                ));\n            }\n\n            // Load-bearing: the pinned inode's real path must still be exactly\n            // the approved canonical path. If any component became a symlink\n            // after approval, the fd resolves elsewhere and this diverges.\n            let current = std::fs::read_link(format!(\"/proc/self/fd/{}\", fd.as_raw_fd()))?;\n            if current != path {\n                return Err(io::Error::new(\n                    io::ErrorKind::PermissionDenied,\n                    format!(\n                        \"sandbox write grant {} was redirected to {}\",\n                        path.display(),","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/zed-industries/zed/blob/5a9b9558db01a6b906cec2fb70a797affdc58cdd/crates/sandbox/src/util/canonical_path.rs#L132-L168","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nzed --sandbox-write-grant /tmp/project\n\n// after\nzed --sandbox-write-grant \"$(readlink -f /tmp/project)\"","handlingStrategy":"validation","validationCode":"use std::path::Path;\n\nfn validate_write_grant(path: &Path) -> std::io::Result<()> {\n    let meta = std::fs::metadata(path)?; // follows symlinks: stat the target\n    if !meta.is_dir() {\n        return Err(std::io::Error::new(\n            std::io::ErrorKind::PermissionDenied,\n            format!(\"{} is not a directory\", path.display()),\n        ));\n    }\n    if std::fs::symlink_metadata(path)?.file_type().is_symlink() {\n        let real = std::fs::canonicalize(path)?;\n        eprintln!(\"grant {} is a symlink; use {} instead\", path.display(), real.display());\n    }\n    Ok(())\n}\n// call before handing the path to the sandbox: validate_write_grant(&grant)?;","typeGuard":"fn is_real_directory(path: &std::path::Path) -> bool {\n    std::fs::symlink_metadata(path)\n        .map(|m| m.is_dir() && !m.file_type().is_symlink())\n        .unwrap_or(false)\n}","tryCatchPattern":"match launch_sandboxed(grant_path) {\n    Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied\n        && err.to_string().contains(\"is a symlink\") =>\n    {\n        let real = std::fs::canonicalize(grant_path)?;\n        launch_sandboxed(&real)?\n    }\n    Err(err) => return Err(err.into()),\n    Ok(handle) => handle,\n}","preventionTips":["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."],"tags":["sandbox","security","filesystem","symlink","permissions"],"backgroundTag":"symlink-not-allowed","analyzedSha":"5a9b9558db01a6b906cec2fb70a797affdc58cdd","analyzedAt":"2026-09-05T10:33:00.413Z","contentChangedAt":"2026-09-05T10:33:00.413Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}