garrytan/gstack · error · Error

{{LEARNINGS_SEARCH:query=...}} value must match ${QUERY_SAFE

Error message

{{LEARNINGS_SEARCH:query=...}} value must match ${QUERY_SAFE_RE} (alphanumeric, space, hyphen, underscore). Got: ${JSON.stringify(queryArg)}

What it means

The {{LEARNINGS_SEARCH:query=...}} resolver in scripts/resolvers/learnings.ts:31 validates the query against QUERY_SAFE_RE (`^[A-Za-z0-9 _-]+$`). Anything else throws to prevent shell injection through the generated `--query` flag. An empty query= value is allowed (falls through to no-query).

Source

Thrown at scripts/resolvers/learnings.ts:31

 */
import type { TemplateContext } from './types';

// Whitelist for query= macro values. Allows alphanumeric, space, hyphen, underscore.
// Anything else (e.g. $, backticks, quotes, ;) is a shell-injection vector when the
// emitted bash interpolates the value into `--query "${queryArg}"`. Static template
// queries hand-written in gstack are safe, but the resolver API must defend against
// future contributors writing dangerous values.
const QUERY_SAFE_RE = /^[A-Za-z0-9 _-]+$/;

export function generateLearningsSearch(ctx: TemplateContext, args?: string[]): string {
  // Parse query= arg. Empty value falls through to no-query (principle of least surprise:
  // a stray {{LEARNINGS_SEARCH:query=}} placeholder gets today's behavior, not a build error).
  const queryArg = (args || [])
    .filter(a => a.startsWith('query='))
    .map(a => a.slice(6))
    .filter(Boolean)[0];
  if (queryArg && !QUERY_SAFE_RE.test(queryArg)) {
    throw new Error(
      `{{LEARNINGS_SEARCH:query=...}} value must match ${QUERY_SAFE_RE} (alphanumeric, space, hyphen, underscore). Got: ${JSON.stringify(queryArg)}`
    );
  }
  const queryFlag = queryArg ? ` --query "${queryArg}"` : '';

  if (ctx.host === 'codex') {
    // Codex: simpler version, no cross-project, uses $GSTACK_BIN
    return `## Prior Learnings

Search for relevant learnings from previous sessions on this project:

\`\`\`bash
$GSTACK_BIN/gstack-learnings-search --limit 10${queryFlag} 2>/dev/null || true
\`\`\`

If learnings are found, incorporate them into your analysis. When a review finding
matches a past learning, note it: "Prior learning applied: [key] (confidence N, from [date])"`;
  }

View on GitHub (pinned to 94993f7401)

Solutions

  1. Restrict the query to ASCII alphanumerics, spaces, hyphens, and underscores
  2. Drop punctuation from the query (e.g. `api auth` instead of `api & auth`)
  3. URL-encode elsewhere if a richer query is needed (this resolver does not support encoding)

Example fix

<!-- before -->
{{LEARNINGS_SEARCH:query=api & auth}}
<!-- after -->
{{LEARNINGS_SEARCH:query=api auth}}
Defensive patterns

Strategy: type-guard

Validate before calling

const QUERY_SAFE_RE = /^[A-Za-z0-9 _-]+$/;
if (query && !QUERY_SAFE_RE.test(query)) {
  throw new Error(`query must match ${QUERY_SAFE_RE}`);
}

Type guard

const QUERY_SAFE_RE = /^[A-Za-z0-9 _-]+$/;
const isSafeQuery = (q: string): boolean => QUERY_SAFE_RE.test(q);

Prevention

When it happens

Trigger: Using quotes, semicolons, ampersands, pipes, or backticks in the query= value. Curly/smart quotes pasted from a rich text editor. Non-ASCII characters.

Common situations: Pasting a query from documentation that was authored in a smart-quote editor. Including punctuation like `api & auth`.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/05b7e3263a7f4d67. Report an issue: GitHub.