gastownhall/beads · error

unterminated string starting at position %d

Error message

unterminated string starting at position %d

What it means

A quoted string was opened with " or ' but the input ended (rune 0) before the matching closing quote. readString (internal/query/lexer.go:201) reports the position where the string started, not where EOF was hit.

Source

Thrown at internal/query/lexer.go:201

		if unicode.IsDigit(r) || r == '-' || r == '+' {
			l.backup()
			return l.readNumberOrDuration(startPos)
		}
		if isIdentStart(r) {
			l.backup()
			return l.readIdent(startPos)
		}
		return Token{}, fmt.Errorf("unexpected character %q at position %d", r, startPos)
	}
}

// readString reads a quoted string.
func (l *Lexer) readString(quote rune, startPos int) (Token, error) {
	var sb strings.Builder
	for {
		r := l.next()
		if r == 0 {
			return Token{}, fmt.Errorf("unterminated string starting at position %d", startPos)
		}
		if r == quote {
			return Token{Type: TokenString, Value: sb.String(), Pos: startPos}, nil
		}
		if r == '\\' {
			// Handle escape sequences
			escaped := l.next()
			switch escaped {
			case 'n':
				sb.WriteRune('\n')
			case 't':
				sb.WriteRune('\t')
			case '\\':
				sb.WriteRune('\\')
			case '"':
				sb.WriteRune('"')
			case '\'':
				sb.WriteRune('\'')

View on GitHub (pinned to 71377f2769)

Solutions

  1. Add the matching closing quote at the end of the string value.
  2. If the value itself contains the same quote character, escape it with a backslash: `title="say \"hi\""`.
  3. Check shell quoting: a single-quoted shell string can swallow the query's quotes; wrap the whole query in double quotes at the shell level.
  4. Verify the query text programmatically: count unescaped " and ' characters before calling Parse.

Example fix

// before
query.Parse("title=\"bug report")
// after
query.Parse("title=\"bug report\"")
Defensive patterns

Strategy: validation

Validate before calling

func balancedQuotes(q string) bool {
    var q1, q2 bool
    for i := 0; i < len(q); i++ {
        switch q[i] {
        case '\\':
            i++
        case '\'':
            q1 = !q1
        case '"':
            q2 = !q2
        }
    }
    return !q1 && !q2
}

Try / catch

if _, err := query.Parse(q); err != nil {
    var pos int
    if _, scan := fmt.Sscanf(err.Error(), "unterminated string starting at position %d", &pos); scan == nil {
        return fmt.Errorf("missing closing quote for string at byte %d", pos)
    }
    return err
}

Prevention

When it happens

Trigger: query.Parse with an unbalanced quote: `title="bug report`, `status='open`, or a quote opened and a newline/EOF reached because the closing quote was deleted during editing.

Common situations: Truncated shell commands (closing quote on the same line got eaten by shell quoting rules); hand-edited saved queries; copying text where the trailing quote was lost; embedding values containing quotes without escaping (e.g. `title="say \"hi\""` miscounted).

Related errors


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