{"record":{"id":"61f734883a71ece2","repo":"charmbracelet/vhs","slug":"unable-to-read-file-srcpath","errorCode":null,"errorMessage":"Unable to read file: ${srcPath}","messagePattern":"Unable to read file: (.+?)","errorType":"validation","errorClass":"parser.Error","httpStatus":null,"severity":"error","filePath":"parser/parser.go","lineNumber":703,"sourceCode":"\tif ext != \".tape\" {\n\t\tp.errors = append(p.errors, NewError(p.peek, \"Expected file with .tape extension\"))\n\t\tp.nextToken()\n\t\treturn []Command{cmd}\n\t}\n\n\t// Check if tape exist\n\tif _, err := os.Stat(srcPath); os.IsNotExist(err) { //nolint:gosec\n\t\tnotFoundErr := fmt.Sprintf(\"File %s not found\", srcPath)\n\t\tp.errors = append(p.errors, NewError(p.peek, notFoundErr))\n\t\tp.nextToken()\n\t\treturn []Command{cmd}\n\t}\n\n\t// Check if source tape contains nested Source command\n\td, err := os.ReadFile(srcPath) //nolint:gosec\n\tif err != nil {\n\t\treadErr := fmt.Sprintf(\"Unable to read file: %s\", srcPath)\n\t\tp.errors = append(p.errors, NewError(p.peek, readErr))\n\t\tp.nextToken()\n\t\treturn []Command{cmd}\n\t}\n\n\tsrcTape := string(d)\n\t// Check source tape is NOT empty\n\tif len(srcTape) == 0 {\n\t\treadErr := fmt.Sprintf(\"Source tape: %s is empty\", srcPath)\n\t\tp.errors = append(p.errors, NewError(p.peek, readErr))\n\t\tp.nextToken()\n\t\treturn []Command{cmd}\n\t}\n\n\tsrcLexer := lexer.New(srcTape)\n\tsrcParser := New(srcLexer)\n\n\t// Check not nested source\n\tsrcCmds := srcParser.Parse()","sourceCodeStart":685,"sourceCodeEnd":721,"githubUrl":"https://github.com/charmbracelet/vhs/blob/c073383b5de0b1f57bf514113029c306bc986539/parser/parser.go#L685-L721","documentation":"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.","triggerScenarios":"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.","commonSituations":"Files checked into CI without read permission; sourcing a directory; restricted mount points or container read-only filesystems.","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"],"exampleFix":"// shell\n# before\n-rw------- nested.tape\n// after\nchmod 644 nested.tape","handlingStrategy":"validation","validationCode":"p := \"nested.tape\"\ninfo, err := os.Stat(p)\nif err != nil {\n    return err\n}\nif info.IsDir() {\n    return fmt.Errorf(\"%s is a directory\", p)\n}\nf, err := os.OpenFile(p, os.O_RDONLY, 0)\nif err != nil {\n    return fmt.Errorf(\"unreadable: %w\", err)\n}\nf.Close()","typeGuard":"func tapeReadable(path string) bool {\n    f, err := os.Open(path)\n    if err != nil { return false }\n    defer f.Close()\n    return true\n}","tryCatchPattern":"if err := parse(tape); err != nil && strings.Contains(err.Error(), \"Unable to read file\") {\n    return fmt.Errorf(\"fix permissions for include: %w\", err)\n}","preventionTips":["Ensure checked-in tapes are mode 644","Never source directories","Test container volume mounts for read access"],"tags":["filesystem","permissions","source"],"backgroundTag":"file-read-permission-denied","analyzedSha":"c073383b5de0b1f57bf514113029c306bc986539","analyzedAt":"2026-09-02T01:53:05.682Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T06:17:21.866Z"}