{"record":{"id":"15a93b299cdae561","repo":"charmbracelet/vhs","slug":"expected-file-with-tape-extension","errorCode":null,"errorMessage":"Expected file with .tape extension","messagePattern":"Expected file with \\.tape extension","errorType":"validation","errorClass":"parser.Error","httpStatus":null,"severity":"error","filePath":"parser/parser.go","lineNumber":686,"sourceCode":"// parseSource parses source command.\n// Source command takes a tape path to include in current tape.\n//\n//\tSource <path>\nfunc (p *Parser) parseSource() []Command {\n\tcmd := Command{Type: token.SOURCE}\n\n\tif p.peek.Type != token.STRING {\n\t\tp.errors = append(p.errors, NewError(p.cur, \"Expected path after Source\"))\n\t\tp.nextToken()\n\t\treturn []Command{cmd}\n\t}\n\n\tsrcPath := p.peek.Literal\n\n\t// Check if path has .tape extension\n\text := filepath.Ext(srcPath)\n\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()","sourceCodeStart":668,"sourceCodeEnd":704,"githubUrl":"https://github.com/charmbracelet/vhs/blob/c073383b5de0b1f57bf514113029c306bc986539/parser/parser.go#L668-L704","documentation":"Fires in parseSource when the path passed to the Source command does not have a .tape extension. The parser refuses to include files that are not tape files, emits the error, advances past the token, and returns without loading the file.","triggerScenarios":"Calling Parse() on a tape with `Source \"settings.yml\"`, `Source \"script.sh\"`, or a file with no extension — ext != \".tape\" triggers the error before any file system access.","commonSituations":"Trying to source config files, shell scripts, or YAML; typos like `.taep` or `.tape.txt` (hidden double extension); macOS/editor appending extensions.","solutions":["Rename the included file to have a .tape extension","Point Source at the correct .tape file","Convert the file's content into a .tape file"],"exampleFix":"// before\nSource \"settings.yml\"\n// after\nSource \"settings.tape\"","handlingStrategy":"validation","validationCode":"p := \"partials/settings.tape\"\nif filepath.Ext(p) != \".tape\" {\n    return fmt.Errorf(\"Source target %q must have .tape extension\", p)\n}","typeGuard":"func isTapeFile(path string) bool { return filepath.Ext(path) == \".tape\" }","tryCatchPattern":"if err := parse(tape); err != nil && strings.Contains(err.Error(), \".tape extension\") {\n    return fmt.Errorf(\"rename include to .tape: %w\", err)\n}","preventionTips":["Name all includable files with .tape extension","Watch for hidden double extensions from editors","Centralize include paths as constants ending in .tape"],"tags":["parser","validation","file-extension"],"backgroundTag":"unexpected-file-extension","analyzedSha":"c073383b5de0b1f57bf514113029c306bc986539","analyzedAt":"2026-09-02T01:53:05.682Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T06:17:21.866Z"}