nanocoai/nanoclaw · error
${NANOCLAW_EXTENSION_NS}/context/instructions.md must be a r
Error message
${NANOCLAW_EXTENSION_NS}/context/instructions.md must be a regular file What it means
When reading a NanoClaw plugin/template extension, the loader expects <extension-ns>/context/instructions.md to be a regular file. It exists (fs.existsSync passes) but lstatSync().isFile() is false — meaning it is a symlink, directory, FIFO, or other special file — so the extension is rejected as malformed.
Source
Thrown at src/templates/extension.ts:70
report.push(
`plugin.json: extensions["${NANOCLAW_EXTENSION_NS}"].agentName must be a nonempty string; ignored`,
);
}
for (const key of Object.keys(ours)) {
if (key !== 'agentName') {
report.push(`plugin.json: extensions["${NANOCLAW_EXTENSION_NS}"].${key} is not recognized; ignored`);
}
}
}
}
const extDir = path.join(pluginDir, NANOCLAW_EXTENSION_NS);
const contextDir = path.join(extDir, 'context');
const instructionsFile = path.join(contextDir, 'instructions.md');
let instructions: string | undefined;
if (fs.existsSync(instructionsFile)) {
if (!fs.lstatSync(instructionsFile).isFile()) {
throw new Error(`${NANOCLAW_EXTENSION_NS}/context/instructions.md must be a regular file`);
}
instructions = fs.readFileSync(instructionsFile, 'utf-8').trimEnd();
}
return {
...(agentName === undefined ? {} : { agentName }),
...(instructions === undefined ? {} : { instructions }),
contextExtras: readContextExtras(contextDir),
tasks: readTasks(path.join(extDir, 'tasks'), `${NANOCLAW_EXTENSION_NS}/tasks`),
report,
};
}
/**
* Every context/**\/*.md except the top-level instructions.md, recursively.
* `name` keeps the path relative to context/ so stamping can preserve the
* layout — a reference like `additional_context/faq.md` written in
* instructions.md resolves unchanged in the agent's workspace.View on GitHub (pinned to 294ef2aee8)
Solutions
- Replace the symlink with a real file: cp -L symlink temp && mv temp instructions.md (or remove dotfile stowing for this path).
- If it is a directory, remove it and create a regular file with the instruction text.
- If sharing content across templates is the goal, duplicate the file or generate it at build time instead of symlinking.
Example fix
# before context/instructions.md -> ../../shared-instructions.md (symlink) # after cp -L ../../shared-instructions.md context/instructions.md
Defensive patterns
Strategy: validation
Validate before calling
import * as fs from 'node:fs';
const p = path.join(extDir, 'context', 'instructions.md');
if (fs.existsSync(p) && !fs.statSync(p).isFile()) throw new Error('instructions.md is not a regular file — fix the template layout'); Type guard
const isRegularFile = (p: string): boolean => { try { return fs.lstatSync(p).isFile(); } catch { return false; } }; Try / catch
try { ext = readNanoclawExtension(pluginDir); } catch (err) { if ((err as Error).message.includes('must be a regular file')) throw new Error('Template instructions.md is a symlink/dir — replace with a real file', { cause: err }); throw err; } Prevention
- Don't symlink template files; copy or generate them.
- Run a template lint (stat each expected file) in CI.
When it happens
Trigger: instructions.md being a symlink (e.g. dotfiles-managed config, ln -s into a repo), a directory accidentally created at that path, or a broken symlink that happens to satisfy existsSync via a target somewhere.
Common situations: Users managing templates with GNU stow or symlinking a shared instructions file across templates; a mistaken 'mkdir instructions.md'; package managers that materialize symlinks.
Related errors
- engage_mode '${w.engage_mode}' can never engage on channel '
- chat-sdk bridge instance ${JSON.stringify(config.instance)}
- Unknown provider: ${name}. Registered: ${known}
- no verb "${verbArg}" on ${res.plural} — run `ncl ${res.plura
- --${column.name.replace(/_/g, '-')} must be a number
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/51a31aa426ca68a2.
Report an issue: GitHub.