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
- Sanitize the childRef value before passing it: replace disallowed characters with '-' or '_'.
- Use a slugified version of titles/filenames instead of raw strings.
- Choose a literal childRef that already matches [A-Za-z0-9._-].
- 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
- Slugify any user-provided or derived childRef (replace spaces/slashes with '-').
- Validate childRef against ^[A-Za-z0-9._-]+$ at the CLI boundary.
- Never pass raw filenames, titles, or URLs as childRef.
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
- childRef is empty after variable substitution
- recipe %q has no file contents
- remote name too long (max 64 characters)
- remote name must start with a letter and contain only alphan
- peer name too long (max 64 characters)
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/0122b964f5df9187.
Report an issue: GitHub.