affaan-m/ECC · error
Unknown template variable: ${key}
Error message
Unknown template variable: ${key} What it means
Thrown by renderTemplate() when the launcherCommand template references a {placeholder} that is not present in the variables map. The orchestrator only populates a fixed set of variables per worker (branch_name, handoff_file, repo_root, session_name, status_file, task_file, worker_name, worker_slug, worktree_path, plus their _raw and _sh variants built by buildTemplateVariables), so any other token is treated as a typo.
Source
Thrown at scripts/lib/tmux-worktree-orchestrator.js:23
const { spawnSync } = require('child_process');
function slugify(value, fallback = 'worker') {
const normalized = String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return normalized || fallback;
}
function renderTemplate(template, variables) {
if (typeof template !== 'string' || template.trim().length === 0) {
throw new Error('launcherCommand must be a non-empty string');
}
return template.replace(/\{([a-z_]+)\}/g, (match, key) => {
if (!(key in variables)) {
throw new Error(`Unknown template variable: ${key}`);
}
return String(variables[key]);
});
}
function shellQuote(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}
function formatCommand(program, args) {
return [program, ...args.map(shellQuote)].join(' ');
}
function buildTemplateVariables(values) {
return Object.entries(values).reduce((accumulator, [key, value]) => {
const stringValue = String(value);
const quotedValue = shellQuote(stringValue);
View on GitHub (pinned to 01e15490f0)
Solutions
- Use only the supported tokens: branch_name, handoff_file, repo_root, session_name, status_file, task_file, worker_name, worker_slug, worktree_path (plus _raw and _sh suffixes).
- Check the regex in renderTemplate: only lowercase letters and underscores inside { } are interpolated, anything else is left as-is and will not trigger this error — but a typo'd lowercase token will.
- If you need a custom value, pre-compute it and substitute before passing the template, or extend buildTemplateVariables in scripts/lib/tmux-worktree-orchestrator.js.
- Print Object.keys(templateVariables) in a debug run to see exactly what is available.
Example fix
// before
launcherCommand: 'claude --task {task} --cwd {worktree}'
// -> Unknown template variable: task
// after
launcherCommand: 'claude --task-file {task_file} --cwd {worktree_path}' Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED = ['branch_name','handoff_file','repo_root','session_name','status_file','task_file','worker_name','worker_slug','worktree_path'];
function validateLauncherTemplate(tpl) {
const used = [...tpl.matchAll(/\{([a-z_]+)\}/g)].map(m => m[1]);
const unknown = used.filter(k => !ALLOWED.includes(k) && !ALLOWED.some(a => k === `${a}` || k === `${a}_raw` || k === `${a}_sh`));
if (unknown.length) throw new Error(`Unknown template tokens: ${unknown.join(', ')}`);
}
validateLauncherTemplate(config.launcherCommand); Type guard
const ALLOWED = ['branch_name','handoff_file','repo_root','session_name','status_file','task_file','worker_name','worker_slug','worktree_path'];
function usesOnlyKnownTokens(tpl) {
return ![...tpl.matchAll(/\{([a-z_]+)\}/g)].map(m => m[1])
.some(k => !ALLOWED.includes(k.replace(/_(raw|sh)$/, '')));
} Try / catch
try {
buildOrchestrationPlan(config);
} catch (error) {
if (/Unknown template variable/.test(error.message)) {
// surface the list of allowed variables to the user
throw new Error(`${error.message}. Allowed: ${ALLOWED.join(', ')}`);
}
throw error;
} Prevention
- Pin template tokens to the documented set: branch_name, handoff_file, repo_root, session_name, status_file, task_file, worker_name, worker_slug, worktree_path (plus _raw and _sh suffixes).
- Lint launcher templates in CI against the allowed-token list.
- Remember tokens are case-sensitive and lowercase-only — {Session_name} is left as-is, not interpolated.
- When in doubt, print Object.keys(templateVariables) in a dry-run.
When it happens
Trigger: config.launcherCommand = 'cd {worktree} && claude' (worktree vs worktree_path); using {task} instead of {task_file}; using {repo} instead of {repo_root}; using an uppercase token like {Session_name} (the regex only matches [a-z_]); referencing a variable that exists on a different worker.
Common situations: Documentation drift: an example uses an old variable name; a user invents a placeholder assuming it will be passed through; case-sensitivity issues; a template copied from a different orchestrator.
Related errors
- launcherCommand must be a non-empty string
- buildOrchestrationPlan requires at least one worker
- seedPaths entries must stay inside repoRoot: ${entry}
- Seed path does not exist in repoRoot: ${seedPath}
- Worker ${index + 1} is missing a task
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/a042825c44114fa5.
Report an issue: GitHub.