gohugoio/hugo · error
binary is not a Node.js script
Error message
binary %q is not a Node.js script
What it means
Returned by Exec.Npx when a binary found in node_modules/.bin or PATH cannot be resolved to a Node.js script. resolveNodeBin returns '' for files that are not a symlink to a .js/.mjs/.cjs file, lack a `node` shebang, and have no extractable JS entry point from an npm wrapper script. Npx always invokes Node, so an unresolvable entry point is fatal.
Solutions
- Reinstall Node deps cleanly: remove node_modules and package-lock, then `npm install`.
- Verify the binary is a Node script: `head -1 node_modules/.bin/<name>` should show a node shebang, or the .cmd wrapper should reference a .js entry.
- If a native binary is intended, do not route it through Npx; use Exec.New instead.
- Upgrade the offending npm package; older wrappers (e.g. postcss-cli 7) had different layouts.
Example fix
// before
runner, err := exec.Npx("my-native-tool", args...)
// after
runner, err := exec.New("my-native-tool", args...) Defensive patterns
Strategy: validation
Validate before calling
func isNodeScriptBin(path string) bool {
if p, err := filepath.EvalSymlinks(path); err == nil {
if strings.HasSuffix(p, ".js") || strings.HasSuffix(p, ".mjs") || strings.HasSuffix(p, ".cjs") { return true }
}
f, err := os.Open(path); if err != nil { return false }; defer f.Close()
b := bufio.NewReader(f); l, _ := b.ReadString('\n')
return strings.HasPrefix(l, "#!") && strings.Contains(l, "node")
} Try / catch
r, err := exec.Npx(name, args...)
if err != nil {
if strings.Contains(err.Error(), "is not a Node.js script") {
// fall back to Exec.New for native binaries, or surface install instructions
}
return err
} Prevention
- Pin npm packages to versions known to ship JS wrappers.
- Verify `head -1 node_modules/.bin/<name>` after install.
- Separate native-binary tools from Npx-routed Node tools.
When it happens
Trigger: Calling Exec.Npx(name,...) where `name` resolves to a native binary, a corrupt npm wrapper, a Windows .cmd whose entry-point regex does not match, or a symlink whose target is not a JS file.
Common situations: Hugo running PostCSS/Tailwind/Babel via the js.Bundler pipe or hugo.Stats after a broken `npm install`, a globally installed native tool shadowing the node_modules shim, or a package whose bin field points to a non-JS launcher.
Related errors
AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09).
Data as JSON: /api/errors/c12f91ba58616ccd.
Report an issue: GitHub.
Appendix: source
Thrown at common/hexec/exec.go:217
resolvedBin = p
loc = binaryLocationNodeModules
} else if p, err := exec.LookPath(name); err == nil {
resolvedBin = p
loc = binaryLocationPath
} else {
return nil, &NotFoundError{name: name, method: "in PATH"}
}
scriptPath := resolveNodeBin(resolvedBin)
e.infol.WithFields(logg.Fields{
logg.Field{Name: "location", Value: loc},
logg.Field{Name: "bin", Value: resolvedBin},
logg.Field{Name: "script", Value: scriptPath},
}).Logf("resolve %q", name)
if scriptPath == "" {
return nil, fmt.Errorf("binary %q is not a Node.js script", name)
}
return func(arg2 ...any) (Runner, error) {
return e.newNode(name, scriptPath, arg2...)
}, nil
})
if err != nil {
return nil, err
}
return newRunner(arg...)
}
// newNode runs a Node.js script via "node [--permission <flags>] <scriptPath> <args>".
func (e *Exec) newNode(name, scriptPath string, arg ...any) (Runner, error) {
var allArgs []any
for _, pa := range e.nodePermissionArgs(name, scriptPath) {
allArgs = append(allArgs, pa)View on GitHub (pinned to 52c9bd7908)