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
- chmod the file to be readable by the running user
- Ensure the path points to a regular file, not a directory
- 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
- Ensure checked-in tapes are mode 644
- Never source directories
- Test container volume mounts for read access
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
- File ${srcPath} not found
- Expected path after Source
- Nested Source detected
- ${srcPath} has ${count} errors
AI-assisted analysis of charmbracelet/vhs@c073383b5d (2026-09-02).
Data as JSON: /api/errors/61f734883a71ece2.
Report an issue: GitHub.