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
- Use the canonical form: `#!/usr/bin/env -S deno run` plus simple unscoped flags
- Remove scoped `--deny-*=<path>` flags from doc example shebangs - they are not representable and force invalid
- Quote shebang arguments properly; avoid unterminated quotes
- 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
- Use the canonical `#!/usr/bin/env -S deno run` prefix in doc examples
- Avoid scoped `--deny-*=<path>` flags in example shebangs
- Keep the binary name `deno` in doc-test shebangs; renamed binaries are not recognized
- Run `deno test --doc` locally so shebang problems surface before CI
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
- {} permissions were found in the config file. Did you mean t
- BenchContext::end() has already been invoked
- Expected the second argument to assertSnapshot() to be an op
- Snapshot serializer must return a string
- Missing snapshot file.
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/efe245ea48256b3b.
Report an issue: GitHub.