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
- Restrict the query to ASCII alphanumerics, spaces, hyphens, and underscores
- Drop punctuation from the query (e.g. `api auth` instead of `api & auth`)
- 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
- Hand-write queries using ASCII alphanumerics, spaces, hyphens, underscores only
- Run a linter that catches smart quotes / non-ASCII in .tmpl files
- Avoid punctuation like &, |, ;, quotes in learning queries
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
- Invalid preamble-tier: ${tier} in ${ctx.tmplPath}. Must be 1
- Invalid skill name "${name}". Must be lowercase letters/digi
- Invalid file path in stageSkill: "${relPath}".
- State name must be alphanumeric (a-z, 0-9, _, -)
- Invalid CSS property name: ${property}. Only letters and hyp
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/05b7e3263a7f4d67.
Report an issue: GitHub.