garrytan/gstack · error · Error
commitSkill: staged dir "${opts.stagedDir}" not accessible:
Error message
commitSkill: staged dir "${opts.stagedDir}" not accessible: ${err.code ?? err.message} What it means
Thrown by commitSkill when fs.lstatSync(opts.stagedDir) raised — most commonly ENOENT (the staged dir was already removed) but also EACCES, ENOTDIR, or any other filesystem error. The original error's code is appended if present (e.g. 'ENOENT'), otherwise its message. This guard exists so commitSkill fails cleanly rather than attempting rename on a path that doesn't exist.
Source
Thrown at browse/src/browser-skill-write.ts:134
* - staged dir is a symlink (refuses to follow)
* - resolved destination escapes the tier root (defense in depth)
*/
export function commitSkill(opts: CommitSkillOptions): string {
validateSkillName(opts.name);
const tiers = opts.tiers ?? defaultTierPaths();
const tierRoot = opts.tier === 'project' ? tiers.project : tiers.global;
if (!tierRoot) {
throw new Error(`commitSkill: tier "${opts.tier}" has no resolved path.`);
}
// Refuse to follow a symlinked staging dir — caller should hand us the path
// returned by stageSkill, which is always a real directory.
let stagedStat: fs.Stats;
try {
stagedStat = fs.lstatSync(opts.stagedDir);
} catch (err: any) {
throw new Error(`commitSkill: staged dir "${opts.stagedDir}" not accessible: ${err.code ?? err.message}`);
}
if (stagedStat.isSymbolicLink()) {
throw new Error(`commitSkill: staged dir "${opts.stagedDir}" is a symlink — refusing to commit.`);
}
if (!stagedStat.isDirectory()) {
throw new Error(`commitSkill: staged path "${opts.stagedDir}" is not a directory.`);
}
// Ensure the tier root exists, then resolve its real path so the final
// destination check defends against tierRoot itself being a symlink.
fs.mkdirSync(tierRoot, { recursive: true, mode: 0o755 });
const realTierRoot = fs.realpathSync(tierRoot);
const dest = path.join(realTierRoot, opts.name);
if (!isPathWithin(dest, realTierRoot)) {
// Should be impossible after validateSkillName, but defense in depth.
throw new Error(`commitSkill: destination "${dest}" escapes tier root.`);
}View on GitHub (pinned to 94993f7401)
Solutions
- Check the staged dir still exists: `ls -la <stagedDir>`.
- Ensure discardStaged is not called before commitSkill on the success path.
- If the dir was removed, re-run stageSkill to recreate it.
- Verify permissions: commitSkill needs read+stat on the staged dir.
Defensive patterns
Strategy: try-catch
Validate before calling
import * as fs from 'fs';
function assertStagedDirAccessible(stagedDir: string): void {
try {
fs.lstatSync(stagedDir);
} catch (err: any) {
throw new Error(`Staged dir "${stagedDir}" not accessible: ${err.code ?? err.message}`);
}
}
// before commitSkill:
assertStagedDirAccessible(opts.stagedDir); Try / catch
try {
commitSkill(opts);
} catch (err: any) {
if (/not accessible/.test(err.message)) {
// staged dir was removed mid-flow; re-stage and retry once
const restaged = stageSkill(stageOpts);
commitSkill({ ...opts, stagedDir: restaged });
} else {
throw err;
}
} Prevention
- Do not call discardStaged before commitSkill on the success path.
- Pass the exact path returned by stageSkill through to commitSkill unmodified.
- Avoid concurrent /skillify runs that share the same spawnId prefix.
When it happens
Trigger: commitSkill with a stagedDir that discardStaged already removed (race between a test-failure cleanup and the commit step); a stagedDir string that was never returned by stageSkill (typo, wrong spawn id); permissions lost after staging (chmod, container remount); the wrapper dir skillify-<spawnId>/ was rotated by another process.
Common situations: /skillify flow called discardStaged before commit; concurrent /skillify invocations interfering despite the per-spawn wrapper; tests that hand-construct a stagedDir without creating it; ephemeral filesystem where /tmp was wiped between stage and commit.
Related errors
- Skill "${name}" not found in any tier.
- Skill "${name}" not found.
- Skill "${name}" has no script.test.ts at ${testFile}
- Skill "${opts.skill.name}" missing script.ts at ${scriptPath
- Invalid file path in stageSkill: "${relPath}".
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/7363f7dc70ef21ac.
Report an issue: GitHub.