Description
Custom tool cwd is not kept inside the project.
resolveCwd in source/custom-tools/handler.ts (lines 132-142 on main, v1.30.0) does ${VAR} expansion, path.resolve, then existsSync. If the path exists, that becomes the cwd. There is no realpath and no project-root check.
export function resolveCwd(
configured: string | undefined,
projectRoot: string,
): string {
if (!configured) return projectRoot;
const expanded = expandVars(configured);
const absolute = isAbsolute(expanded)
? expanded
: resolve(projectRoot, expanded);
return existsSync(absolute) ? absolute : projectRoot;
}
File tools already block this. resolveFilePath in source/utils/path-validation.ts (around 156) follows symlinks and throws if the real path leaves the project. Custom tools never got that check.
The docs example is cwd: ./scripts. If scripts is a symlink to a folder outside the repo, the shell starts there. It still looks like an in-repo path, so nothing is logged. Same thing with an absolute path or ${HOME}.
handler.spec.ts only covers the missing-path fallback (lines 63-66), so CI stays green.
Environment
- OS: macOS (darwin 25.5.0)
- Node version: v26.7.0
- Nanocoder version: 1.30.0 (main @ 1d0cf10)
- Provider: N/A
- Model: N/A
Steps to Reproduce
- mkdir -p /tmp/nc-cwd-proj /tmp/nc-cwd-out && ln -s /tmp/nc-cwd-out /tmp/nc-cwd-proj/scripts
- Call resolveCwd with cwd
./scripts and project root /tmp/nc-cwd-proj
- Repeat with cwd set to
/tmp/nc-cwd-out, then with ${HOME}
Expected Behavior
cwd stays inside the project after symlink resolution. If the real path is outside, fall back to the project root, same as a missing path. Reuse the file-tool check (resolveFilePath / getContainedSessionCwd).
Actual Behavior
./scripts is a symlink, so the shell cwd is outside the repo. Absolute paths and ${HOME} do the same. No warning.
Logs/Screenshots
viaSymlink : /tmp/nc-cwd-proj/scripts
symlinkReal : /private/tmp/nc-cwd-out
escaped : true
absolute cwd=/tmp/nc-cwd-out
absEscaped : true
missing path : falls back to projectRoot (the existing test)
Additional Context
Small fix: run the resolved cwd through the same realpath + containment check as resolveFilePath / getContainedSessionCwd. If it is outside, return projectRoot.
Would add tests for symlink-out, absolute-out, ${HOME}, and a normal in-project relative path.
I can open a PR if this gets assigned.
Description
Custom tool
cwdis not kept inside the project.resolveCwdinsource/custom-tools/handler.ts(lines 132-142 on main, v1.30.0) does${VAR}expansion,path.resolve, thenexistsSync. If the path exists, that becomes the cwd. There is norealpathand no project-root check.File tools already block this.
resolveFilePathinsource/utils/path-validation.ts(around 156) follows symlinks and throws if the real path leaves the project. Custom tools never got that check.The docs example is
cwd: ./scripts. Ifscriptsis a symlink to a folder outside the repo, the shell starts there. It still looks like an in-repo path, so nothing is logged. Same thing with an absolute path or${HOME}.handler.spec.tsonly covers the missing-path fallback (lines 63-66), so CI stays green.Environment
Steps to Reproduce
./scriptsand project root/tmp/nc-cwd-proj/tmp/nc-cwd-out, then with${HOME}Expected Behavior
cwd stays inside the project after symlink resolution. If the real path is outside, fall back to the project root, same as a missing path. Reuse the file-tool check (
resolveFilePath/getContainedSessionCwd).Actual Behavior
./scriptsis a symlink, so the shell cwd is outside the repo. Absolute paths and${HOME}do the same. No warning.Logs/Screenshots
Additional Context
Small fix: run the resolved cwd through the same realpath + containment check as
resolveFilePath/getContainedSessionCwd. If it is outside, returnprojectRoot.Would add tests for symlink-out, absolute-out,
${HOME}, and a normal in-project relative path.I can open a PR if this gets assigned.