denoland/deno · error · Error

invalid doc test hashbang: {} ({reason})

Error message

invalid doc test hashbang: {} ({reason})

What it means

`deno test` extracts fenced code blocks from JSDoc @example tags and markdown docs and runs them as doc tests. The block's shebang (e.g. `#!/usr/bin/env -S deno run --allow-read`) is parsed by parse_shebang in cli/util/extract.rs to derive the Deno.test permissions for the generated test; when the shebang cannot be parsed or represented, the test is marked invalid (forcing failure) with this message plus a parenthesized reason.

Source

Thrown at cli/util/extract.rs:557

  Invalid(String),
}

/// Parses a shebang line like `#!/usr/bin/env -S deno run --allow-read`.
///
/// The flags are parsed using deno's own CLI argument parser and the resulting
/// permissions are forwarded to `Deno.test`.
///
/// Known limitations:
/// - The `deno` executable is matched by file name, so custom-named binaries
///   (e.g. `deno-canary`) are not recognized and yield an invalid shebang.
/// - A scoped `--deny-*=<path>` cannot be represented in the `Deno.test`
///   permissions object yet and yields an invalid shebang to force failure
/// - A `--ignore-*` cannnot be represented in the `Deno.test` permissions
///   object either yet, so they are currently ignored
fn parse_shebang(shebang: &str) -> Shebang {
  let invalid = |reason: &str| {
    Shebang::Invalid(format!(
      "invalid doc test hashbang: {} ({reason})",
      shebang.trim()
    ))
  };
  let Some(line) = shebang.trim_start().strip_prefix("#!") else {
    return invalid("invalid hashbang");
  };
  let Some(tokens) = shlex::split(line) else {
    return invalid("tokenization failed, possibly due to unterminated quotes");
  };
  // Find the `deno` executable in the shebang (e.g. `deno`, `/usr/bin/deno`).
  let Some(deno_index) = tokens.iter().position(|token| {
    std::path::Path::new(token)
      .file_stem()
      .and_then(|stem| stem.to_str())
      == Some("deno")
  }) else {
    return invalid("binary basename needs to be 'deno'");
  };

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Use the canonical form: `#!/usr/bin/env -S deno run` plus simple unscoped flags
  2. Remove scoped `--deny-*=<path>` flags from doc example shebangs - they are not representable and force invalid
  3. Quote shebang arguments properly; avoid unterminated quotes
  4. Do not rely on the binary being named exactly `deno` - renamed binaries are not recognized

Example fix

# before
#!/usr/bin/env -S deno run --deny-read=/etc

# after
#!/usr/bin/env -S deno run --allow-read
Defensive patterns

Strategy: validation

Validate before calling

function checkShebang(line: string): string | null {
  const s = line.trim();
  if (!s.startsWith("#!/usr/bin/env -S deno")) return "missing canonical deno shebang";
  if (/['\"]/.test(s) && !/(['\"]).*?\1/.test(s)) return "unterminated quote";
  return null; // ok
}

Prevention

When it happens

Trigger: Shebang not starting with '#!' after trimming; shell tokenization failure (unterminated quotes); no token naming the `deno` executable (e.g. a custom binary name like `deno-canary`); scoped `--deny-*=<path>` permissions that the permissions object cannot express yet.

Common situations: Writing doc examples with unusual shebangs; CI environments using a renamed deno binary; documenting scoped deny flags or ignore flags in example shebangs.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/efe245ea48256b3b. Report an issue: GitHub.