larksuite/cli · error
file has multiple hard links, so the other names it can be r
Error message
file has multiple hard links, so the other names it can be reached by cannot be checked (hint: copy the file and use the copy instead)
What it means
The opened file's st.Nlink > 1 and hard-link rejection is enabled (openValidated always passes true). An extra name lets a denylisted file be smuggled into an allowed directory, and the filesystem cannot enumerate the other names, so the library bluntly refuses any multi-linked file — including benign layouts like pnpm/nix content-addressed stores.
Source
Thrown at internal/vfs/localfileio/openvalidated_unix.go:71
return f, nil
}
// inspectOpenedFile validates the opened fd and restores blocking mode. pre is
// nil when there is no prior Stat to compare against.
func inspectOpenedFile(f *os.File, pre os.FileInfo, rejectHardLinks bool) error {
post, err := f.Stat()
if err != nil {
return fmt.Errorf("cannot stat opened file: %w", err)
}
if pre != nil && !os.SameFile(pre, post) {
return fmt.Errorf("file changed between validation and open")
}
if !post.Mode().IsRegular() {
return fmt.Errorf("not a regular file (directories, devices, FIFOs, and sockets are refused)")
}
if rejectHardLinks {
if st, ok := post.Sys().(*syscall.Stat_t); ok && st.Nlink > 1 {
return fmt.Errorf("file has multiple hard links, so the other names it can be reached by " +
"cannot be checked (hint: copy the file and use the copy instead)")
}
}
if err := syscall.SetNonblock(int(f.Fd()), false); err != nil {
return fmt.Errorf("cannot restore blocking mode: %w", err)
}
return nil
}
View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Copy the file (cp, not ln) and use the copy — this is the documented workaround
- If the file lives in a pnpm/nix-style store, resolve to the real file and copy it out, or reinstall with a non-hardlink layout (e.g. pnpm with hoisted node-linker)
- Check with stat: if Links is 1 the error was stale/re-raced; retry
- Avoid hardlink-based dedup tools on directories the CLI must read
Example fix
// before ln config.yaml /workdir/config.yaml # Nlink=2, refused // after cp config.yaml /workdir/config.yaml # independent inode, accepted
Defensive patterns
Strategy: validation
Validate before calling
info, err := os.Stat(path)
if err != nil { return err }
if st, ok := info.Sys().(*syscall.Stat_t); ok && st.Nlink > 1 {
// copy to a fresh file before use
tmp := path + ".copy"
if err := copyFile(path, tmp); err != nil { return err }
path = tmp
} Try / catch
var pve *fileio.PathValidationError
if errors.As(err, &pve) && strings.Contains(pve.Error(), "multiple hard links") { /* copy the file and retry with the copy */ } Prevention
- Use cp, never ln, when staging files the CLI reads
- Stat the file first and refuse Nlink>1 (unix: info.Sys().(*syscall.Stat_t).Nlink)
- Avoid reading directly from pnpm/nix content-addressed stores; copy out first
- Skip hardlink-dedup (rsync --link-dest) in directories the CLI targets
When it happens
Trigger: Any openValidated read of a file whose inode has 2+ directory entries: hard links created with ln, or content-addressed package stores (pnpm, nix, cargo hardlink caches) linking into a shared store outside the allowlist.
Common situations: Reading a file inside a pnpm node_modules or nix store path; a hardlink-based dedup/backup tool (rsync --link-dest) touched the file; user created a hard link for convenience.
Related errors
- %s %q has multiple hard links, so writing it would also rewr
- %s: path must be absolute, got %q
- %s: cannot stat %q: %w
- %s: path %q is a directory, not a file
- %s: path %q is a symlink (not allowed)
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/8146b1b8466705f9.
Report an issue: GitHub.