{"record":{"id":"876e3fd3a4e3337c","repo":"nanocoai/nanoclaw","slug":"template-ref-must-be-relative-to-the-templates-dir","errorCode":null,"errorMessage":"Template ref must be relative to the templates directory: \"${ref}\"","messagePattern":"Template ref must be relative to the templates directory: \"(.+?)\"","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/templates/local-dir.ts","lineNumber":22,"sourceCode":"import { 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":4,"sourceCodeEnd":34,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/templates/local-dir.ts#L4-L34","documentation":"resolveLocalTemplate only accepts refs relative to the templates directory. This error fires when the ref is an absolute path ('/etc/templates/x') or starts with '~' — both would bypass the intended base directory, so they are rejected up front. This is both a safety boundary and a UX guard forcing template refs to be names relative to TEMPLATES_DIR.","triggerScenarios":"Calling resolveLocalTemplate('/home/me/templates/my-tpl') or '~/my-tpl'; building the ref by concatenating an absolute base path with a template name; users pasting a full path copied from a file explorer.","commonSituations":"Users pasting absolute paths into a --template flag; scripts that resolve a path first and then pass the resolved absolute path back into the resolver; '~' expansion done manually before calling.","solutions":["Pass only the template's relative name: resolveLocalTemplate('my-tpl').","If you have an absolute path, either strip the TEMPLATES_DIR prefix or bypass the resolver and read the directory directly.","Don't pre-expand '~' — pass the bare relative ref."],"exampleFix":"// before\nresolveLocalTemplate('/opt/nanoclaw/templates/basic-agent');\n\n// after\nresolveLocalTemplate('basic-agent');","handlingStrategy":"type-guard","validationCode":"const isRelativeRef = (r: string): boolean => r.length > 0 && !path.isAbsolute(r) && !r.startsWith('~');\nif (!isRelativeRef(ref)) throw new Error('template ref must be a name relative to the templates dir');","typeGuard":"const isRelativeTemplateRef = (r: unknown): r is string => typeof r === 'string' && r.length > 0 && !path.isAbsolute(r) && !r.startsWith('~');","tryCatchPattern":null,"preventionTips":["Pass template names, never paths, through user-facing flags.","Document that refs resolve against TEMPLATES_DIR only."],"tags":["templates","path-validation","security"],"backgroundTag":"absolute-path-rejected","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}