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

  1. Reinstall Node deps cleanly: remove node_modules and package-lock, then `npm install`.
  2. 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.
  3. If a native binary is intended, do not route it through Npx; use Exec.New instead.
  4. 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

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)