{"record":{"id":"adc5b752ea372683","repo":"nanocoai/nanoclaw","slug":"invalid-template-ref-ref","errorCode":null,"errorMessage":"Invalid template ref: \"${ref}\"","messagePattern":"Invalid template ref: \"(.+?)\"","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/templates/local-dir.ts","lineNumber":19,"sourceCode":"import fs from 'fs';\nimport path from 'path';\n\nimport { TEMPLATES_DIR } from '../config.js';\n\n/**\n * Resolve a LOCAL template ref to an absolute directory under `base`\n * (TEMPLATES_DIR by default). Lexical containment only — no realpathSync, no\n * symlink resolution (out of threat model). Mirrors ensureWithinBase() in\n * group-folder.ts. Refs are legitimately multi-segment (e.g. \"sales/sdr\"), so\n * this does NOT reuse isValidGroupFolder (which rejects \"/\").\n *\n * Rejects: empty / untrimmed refs, absolute paths, a leading \"~\", and any ref\n * that escapes `base` after resolution. Throws if the resolved path is missing\n * or not a directory.\n */\nexport function resolveLocalTemplate(ref: string, base: string = TEMPLATES_DIR): string {\n  if (!ref || ref !== ref.trim()) {\n    throw new Error(`Invalid template ref: \"${ref}\"`);\n  }\n  if (path.isAbsolute(ref) || ref.startsWith('~')) {\n    throw new Error(`Template ref must be relative to the templates directory: \"${ref}\"`);\n  }\n  const candidate = path.resolve(base, ref);\n  const rel = path.relative(base, candidate);\n  if (rel.startsWith('..') || path.isAbsolute(rel)) {\n    throw new Error(`Template ref escapes the templates directory: \"${ref}\"`);\n  }\n  if (!fs.existsSync(candidate) || !fs.statSync(candidate).isDirectory()) {\n    throw new Error(`Template not found: \"${ref}\" (looked in ${base})`);\n  }\n  return candidate;\n}\n","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/templates/local-dir.ts#L1-L34","documentation":"resolveLocalTemplate validates the user-supplied template ref before resolving it against the templates directory. This variant throws when the ref is empty or has leading/trailing whitespace — refs must be exact, non-empty relative names. (A separate error covers absolute paths and '~', and another covers escaping.)","triggerScenarios":"Calling with ref = '', ' basic-agent ', or a value built from untrimmed user input (CLI arg, config field) that carries whitespace; also undefined/null coerced to a string.","commonSituations":"Passing a template name straight from a config file or CLI argument without trimming; copy-pasting a template name with a trailing newline from a list; template lists containing an empty entry.","solutions":["Trim before calling: resolveLocalTemplate(ref.trim()).","Validate the ref is non-empty after trimming and surface a friendly CLI error first.","Sanitize template lists in config so they contain no blank entries."],"exampleFix":"// before\nresolveLocalTemplate(rawArg);\n\n// after\nresolveLocalTemplate(rawArg?.trim() ?? '').","handlingStrategy":"validation","validationCode":"const ref = rawRef?.trim() ?? '';\nif (!ref) throw new Error('template ref required');\nif (!/^[a-z0-9][a-z0-9._/-]*$/i.test(ref)) throw new Error(`bad template ref: \"${ref}\"`);","typeGuard":"const isValidTemplateRef = (r: unknown): r is string => typeof r === 'string' && r.length > 0 && r === r.trim() && !r.startsWith('/') && !r.startsWith('~');","tryCatchPattern":null,"preventionTips":["Trim CLI/config inputs once at the boundary.","Filter empty entries out of template lists."],"tags":["templates","input-validation"],"backgroundTag":"invalid-path-reference","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}