charmbracelet/vhs · error · parser.Error

Expected file with .tape extension

Error message

Expected file with .tape extension

What it means

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.

Solutions

  1. Rename the included file to have a .tape extension
  2. Point Source at the correct .tape file
  3. Convert the file's content into a .tape file

Example fix

// before
Source "settings.yml"
// after
Source "settings.tape"
Defensive patterns

Strategy: validation

Validate before calling

p := "partials/settings.tape"
if filepath.Ext(p) != ".tape" {
    return fmt.Errorf("Source target %q must have .tape extension", p)
}

Type guard

func isTapeFile(path string) bool { return filepath.Ext(path) == ".tape" }

Try / catch

if err := parse(tape); err != nil && strings.Contains(err.Error(), ".tape extension") {
    return fmt.Errorf("rename include to .tape: %w", err)
}

Prevention

When it happens

Trigger: 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.

Common situations: Trying to source config files, shell scripts, or YAML; typos like `.taep` or `.tape.txt` (hidden double extension); macOS/editor appending extensions.

Related errors


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

Appendix: source

Thrown at parser/parser.go:686

// parseSource parses source command.
// Source command takes a tape path to include in current tape.
//
//	Source <path>
func (p *Parser) parseSource() []Command {
	cmd := Command{Type: token.SOURCE}

	if p.peek.Type != token.STRING {
		p.errors = append(p.errors, NewError(p.cur, "Expected path after Source"))
		p.nextToken()
		return []Command{cmd}
	}

	srcPath := p.peek.Literal

	// Check if path has .tape extension
	ext := filepath.Ext(srcPath)
	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()

View on GitHub (pinned to c073383b5d)