* feat(#52): add agent_skills_security.trusted_global_roots allowlist Opt-in allowlist so a global: agent skill whose SKILL.md realpath resolves outside the default global skills base (e.g. ~/.claude/skills) is accepted when its real target lies under a user-declared trusted root. Default [] is byte-identical to prior behavior; the symlink-escape guard is preserved and simply re-applied against each declared root. - src/security.cts: loadTrustedGlobalRoots — tilde-expand (~ and ~/), reject project-relative and dangerously broad roots (filesystem/UNC root, homedir), realpath-canonicalize each root every run and drop non-existent ones. - src/init.cts: on base-check failure the guard consults the trusted roots (hoisted out of the loop); emits a stderr NOTE when a skill is accepted via a trusted root so the widened boundary is visible. - src/core.cts: thread agent_skills_security through loadConfig. - config-schema.manifest.json: allow the new key path. - docs/CONFIGURATION.md: document the option and its security model. - tests/agent-skills.test.cjs: unit + end-to-end CLI coverage (regression, feature, negative, broad-root hardening, stderr NOTE). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#52): add changeset fragment for trusted_global_roots (#754) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
23
src/init.cts
23
src/init.cts
@@ -21,7 +21,7 @@ import { stateExtractField } from './state-document.cjs';
|
||||
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- commands.cjs is an export= CommonJS module
|
||||
import commandsMod = require('./commands.cjs');
|
||||
import { validatePath } from './security.cjs';
|
||||
import { validatePath, loadTrustedGlobalRoots } from './security.cjs';
|
||||
import { getGlobalSkillDir, getGlobalSkillDisplayPath, getGlobalSkillsBase } from './runtime-homes.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
||||
import frontmatterMod = require('./frontmatter.cjs');
|
||||
@@ -1870,6 +1870,12 @@ function buildAgentSkillsBlock(
|
||||
if (typeof skillPaths === 'string') skillPaths = [skillPaths];
|
||||
if (!Array.isArray(skillPaths) || skillPaths.length === 0) return '';
|
||||
|
||||
// Hoist trusted roots computation before the loop: loadTrustedGlobalRoots does
|
||||
// realpathSync I/O and should run at most once per call, not once per failing skill.
|
||||
// It returns [] cheaply when no roots are configured, so the realpath cost only
|
||||
// occurs when the caller has actually set trusted_global_roots.
|
||||
const trustedGlobalRoots = loadTrustedGlobalRoots(config);
|
||||
|
||||
const validPaths: { ref: string; display: string }[] = [];
|
||||
for (const skillPath of skillPaths) {
|
||||
if (typeof skillPath !== 'string') continue;
|
||||
@@ -1905,10 +1911,17 @@ function buildAgentSkillsBlock(
|
||||
}
|
||||
const pathCheck = validatePath(globalSkillMd, globalSkillsBase, { allowAbsolute: true }) as unknown as Record<string, unknown>;
|
||||
if (!pathCheck['safe']) {
|
||||
process.stderr.write(
|
||||
`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`,
|
||||
);
|
||||
continue;
|
||||
const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => {
|
||||
const rootCheck = validatePath(globalSkillMd, root, { allowAbsolute: true }) as unknown as Record<string, unknown>;
|
||||
return Boolean(rootCheck['safe']);
|
||||
});
|
||||
if (!acceptedViaTrustedRoot) {
|
||||
process.stderr.write(
|
||||
`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
|
||||
}
|
||||
validPaths.push({ ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user