gastownhall/beads · error

invalid childRef '%s': must be alphanumeric, dash, underscor

Error message

invalid childRef '%s': must be alphanumeric, dash, underscore, or dot only

What it means

After substituting variables, generateBondedID validates childRef against bondedIDPattern (alphanumeric, dash, underscore, dot only). If the resolved childRef contains other characters — spaces, slashes, '@', etc. — it throws 'invalid childRef %s: must be alphanumeric, dash, underscore, or dot only', because the result would form an invalid issue ID when combined with the parent ID.

Source

Thrown at cmd/bd/template.go:516

//   - Root: parent.childref (e.g., "patrol-x7k.arm-ace")
//   - Children: parent.childref.step (e.g., "patrol-x7k.arm-ace.capture")
//
// The childRef is variable-substituted before use.
// Returns empty string if not a bonded operation (opts.ParentID empty).
func generateBondedID(oldID string, rootID string, opts CloneOptions) (string, error) {
	if opts.ParentID == "" {
		return "", nil // Not a bonded operation
	}

	// Substitute variables in childRef
	childRef := substituteVariables(opts.ChildRef, opts.Vars)

	// Validate childRef after substitution
	if childRef == "" {
		return "", fmt.Errorf("childRef is empty after variable substitution")
	}
	if !bondedIDPattern.MatchString(childRef) {
		return "", fmt.Errorf("invalid childRef '%s': must be alphanumeric, dash, underscore, or dot only", childRef)
	}

	if oldID == rootID {
		// Root issue: parent.childref
		newID := fmt.Sprintf("%s.%s", opts.ParentID, childRef)
		return newID, nil
	}

	// Child issue: parent.childref.relative
	// Extract the relative portion of the old ID (part after root)
	relativeID := getRelativeID(oldID, rootID)
	if relativeID == "" {
		// No hierarchical relationship - use a suffix from the old ID to ensure uniqueness.
		// Extract the last part of the old ID (after any prefix or dash)
		suffix := extractIDSuffix(oldID)
		newID := fmt.Sprintf("%s.%s.%s", opts.ParentID, childRef, suffix)
		return newID, nil
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Sanitize the childRef value before passing it: replace disallowed characters with '-' or '_'.
  2. Use a slugified version of titles/filenames instead of raw strings.
  3. Choose a literal childRef that already matches [A-Za-z0-9._-].
  4. Validate user input at the CLI boundary before it reaches CloneOptions.

Example fix

// before
opts := CloneOptions{ChildRef: "fix login page"} // space invalid
// after
opts := CloneOptions{ChildRef: "fix-login-page"}
Defensive patterns

Strategy: validation

Validate before calling

var bondedIDPattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
ref := substituteVariables(opts.ChildRef, opts.Vars)
if !bondedIDPattern.MatchString(ref) {
	return fmt.Errorf("childRef %q contains invalid characters", ref)
}

Try / catch

newID, err := generateBondedID(ctx, tx, oldID, rootID, opts)
if err != nil && strings.Contains(err.Error(), "invalid childRef") {
	return fmt.Errorf("sanitize childRef to [A-Za-z0-9._-]: %w", err)
}

Prevention

When it happens

Trigger: Calling cloneSubgraphInto / generateBondedID with a ChildRef (literal or post-substitution) containing forbidden characters: 'my task' (space), 'a/b' (slash), '{{name}}' resolving to a value with spaces or punctuation.

Common situations: Deriving childRef from free-form user input or issue titles that contain spaces/slashes; variable values pulled from filenames or URLs; shell-quoted values retaining special characters.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/0122b964f5df9187. Report an issue: GitHub.