gastownhall/beads · error

reading reason file %q: %w

Error message

reading reason file %q: %w

What it means

When --reason-file is given, resolveReasonFile reads it via readBodyFile and wraps any read failure with the offending path (`reading reason file %q: %w`). This surfaces filesystem-level problems (missing file, permission denied, read error) in the context of the close command.

Source

Thrown at cmd/bd/close.go:681

}

// resolveReasonFile resolves the --reason-file flag for `bd close`.
// Returns (content, true, nil) when --reason-file was set and read successfully.
// Returns (_, false, nil) when --reason-file was not set.
// Returns an error on conflict with an existing reason, file read failure, or empty content.
// Mirrors the --body-file pattern from `bd create` so agents can pass structured close
// templates without shell-escaping hell.
func resolveReasonFile(cmd *cobra.Command, hasExistingReason bool) (string, bool, error) {
	if !cmd.Flags().Changed("reason-file") {
		return "", false, nil
	}
	if hasExistingReason {
		return "", false, fmt.Errorf("cannot specify both --reason-file and --reason/--resolution/--message/--comment")
	}
	path, _ := cmd.Flags().GetString("reason-file")
	content, err := readBodyFile(path)
	if err != nil {
		return "", false, fmt.Errorf("reading reason file %q: %w", path, err)
	}
	if strings.TrimSpace(content) == "" {
		return "", false, fmt.Errorf("--reason-file %q is empty; close reason is required", path)
	}
	return content, true, nil
}

// resolveCloseTargets resolves a batch of partial issue IDs for `bd close`,
// preserving input order. For each ID it tries the local store first, then
// explicit prefix routing via routes.jsonl, then a shared contributor-routed
// store. This matches resolveAndGetIssueWithRouting's routing precedence.
//
// The contributor-routed handle is shared across the batch so bulk close does
// not repeatedly open the same planning store and every result has a clear store
// owner for subsequent close-time checks and writes.
//
// Each returned RoutedResult.Store points to whichever store actually owns the
// issue. The caller invokes cleanup() once when done; per-result Close() is a

View on GitHub (pinned to 71377f2769)

Solutions

  1. Verify the path exists and is readable (`cat <path>` or `test -r <path>`).
  2. Use an absolute path or correct the relative path.
  3. If '-' is intended to read stdin, pass --reason-file - explicitly.

Example fix

// before
bd close bd-1 --reason-file ./notes.txt  # file absent
// after
bd close bd-1 --reason-file /abs/path/notes.txt  # ensure file exists first
Defensive patterns

Strategy: validation

Validate before calling

test -r "$REASON_FILE" || { echo "reason file missing: $REASON_FILE" >&2; exit 1; }

Try / catch

if err := runClose(...); err != nil {
	var wrapped string
	if strings.Contains(err.Error(), "reading reason file") {
		// check path existence/permissions, then retry
	}
	_ = wrapped
}

Prevention

When it happens

Trigger: --reason-file points to a nonexistent file, an unreadable path, or a directory; readBodyFile returns any I/O error.

Common situations: Typo in the file path; running from a different working directory with a relative path; file deleted between scripting steps; permission restrictions in CI.

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 gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/41c1df57338b984d. Report an issue: GitHub.