charmbracelet/vhs · error · parser.Error

Unable to read file: ${srcPath}

Error message

Unable to read file: ${srcPath}

What it means

Fires in parseSource when the .tape file exists (os.Stat succeeded) but os.ReadFile fails to read it — e.g. permission denied, or the path is a directory. The error is recorded and the included file's commands are skipped.

Source

Thrown at parser/parser.go:703

	if ext != ".tape" {
		p.errors = append(p.errors, NewError(p.peek, "Expected file with .tape extension"))
		p.nextToken()
		return []Command{cmd}
	}

	// Check if tape exist
	if _, err := os.Stat(srcPath); os.IsNotExist(err) { //nolint:gosec
		notFoundErr := fmt.Sprintf("File %s not found", srcPath)
		p.errors = append(p.errors, NewError(p.peek, notFoundErr))
		p.nextToken()
		return []Command{cmd}
	}

	// Check if source tape contains nested Source command
	d, err := os.ReadFile(srcPath) //nolint:gosec
	if err != nil {
		readErr := fmt.Sprintf("Unable to read file: %s", srcPath)
		p.errors = append(p.errors, NewError(p.peek, readErr))
		p.nextToken()
		return []Command{cmd}
	}

	srcTape := string(d)
	// Check source tape is NOT empty
	if len(srcTape) == 0 {
		readErr := fmt.Sprintf("Source tape: %s is empty", srcPath)
		p.errors = append(p.errors, NewError(p.peek, readErr))
		p.nextToken()
		return []Command{cmd}
	}

	srcLexer := lexer.New(srcTape)
	srcParser := New(srcLexer)

	// Check not nested source
	srcCmds := srcParser.Parse()

View on GitHub (pinned to c073383b5d)

Solutions

  1. chmod the file to be readable by the running user
  2. Ensure the path points to a regular file, not a directory
  3. Check container/volume mount permissions

Example fix

// shell
# before
-rw------- nested.tape
// after
chmod 644 nested.tape
Defensive patterns

Strategy: validation

Validate before calling

p := "nested.tape"
info, err := os.Stat(p)
if err != nil {
    return err
}
if info.IsDir() {
    return fmt.Errorf("%s is a directory", p)
}
f, err := os.OpenFile(p, os.O_RDONLY, 0)
if err != nil {
    return fmt.Errorf("unreadable: %w", err)
}
f.Close()

Type guard

func tapeReadable(path string) bool {
    f, err := os.Open(path)
    if err != nil { return false }
    defer f.Close()
    return true
}

Try / catch

if err := parse(tape); err != nil && strings.Contains(err.Error(), "Unable to read file") {
    return fmt.Errorf("fix permissions for include: %w", err)
}

Prevention

When it happens

Trigger: Parse() on a tape whose Source target is unreadable: chmod 000 file, the path is a directory with a .tape-looking name, or a filesystem/SELinux error between Stat and ReadFile.

Common situations: Files checked into CI without read permission; sourcing a directory; restricted mount points or container read-only filesystems.

Related errors


AI-assisted analysis of charmbracelet/vhs@c073383b5d (2026-09-02). Data as JSON: /api/errors/61f734883a71ece2. Report an issue: GitHub.