charmbracelet/vhs · error · parser.Error

File ${srcPath} not found

Error message

File ${srcPath} not found

What it means

Fires in parseSource when os.Stat reports the referenced .tape file does not exist (os.IsNotExist). The Source command cannot be resolved, the error is recorded, and the parser moves on without including the file's commands.

Source

Thrown at parser/parser.go:694

		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()
		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))

View on GitHub (pinned to c073383b5d)

Solutions

  1. Fix the path — note it is resolved relative to the process working directory, not the parent tape
  2. Create the missing .tape file
  3. Remove the Source line if the file is not needed

Example fix

// before
Source "nested.tape"  (file absent)
// after
mkdir -p demo && move nested.tape demo/nested.tape; Source "demo/nested.tape"
Defensive patterns

Strategy: validation

Validate before calling

p := "nested.tape"
if _, err := os.Stat(p); os.IsNotExist(err) {
    return fmt.Errorf("Source target %q does not exist (cwd=%s)", p, mustCwd())
}

Type guard

func tapeExists(path string) bool {
    info, err := os.Stat(path)
    return err == nil && !info.IsDir()
}

Try / catch

if err := parse(tape); err != nil && strings.Contains(err.Error(), "not found") {
    return fmt.Errorf("missing include referenced by %s: %w", tape, err)
}

Prevention

When it happens

Trigger: Parse() on a tape with `Source "nested.tape"` where nested.tape is absent from the working directory (or the path is relative to the CWD, not the parent tape's directory).

Common situations: Running vhs from a different directory than where the tape lives; CI checkouts that omit the nested tape; typos in the path; case-sensitive filesystems.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


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