{"record":{"id":"e4600694923e0db9","repo":"google-gemini/gemini-cli","slug":"skill-linking-cancelled-by-user","errorCode":null,"errorMessage":"Skill linking cancelled by user.","messagePattern":"Skill linking cancelled by user\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"packages/cli/src/utils/skillUtils.ts","lineNumber":250,"sourceCode":"  const seenNames = new Map<string, string>();\n  for (const skill of skills) {\n    if (seenNames.has(skill.name)) {\n      throw new Error(\n        `Duplicate skill name \"${skill.name}\" found at multiple locations:\\n  - ${seenNames.get(skill.name)}\\n  - ${skill.location}`,\n      );\n    }\n    seenNames.set(skill.name, skill.location);\n  }\n\n  const workspaceDir = process.cwd();\n  const storage = new Storage(workspaceDir);\n  const targetDir =\n    scope === 'workspace'\n      ? storage.getProjectSkillsDir()\n      : Storage.getUserSkillsDir();\n\n  if (!(await requestConsent(skills, targetDir))) {\n    throw new Error('Skill linking cancelled by user.');\n  }\n\n  const resolvedTarget = path.resolve(targetDir);\n  await fs.mkdir(resolvedTarget, { recursive: true });\n\n  const linkedSkills: Array<{ name: string; location: string }> = [];\n\n  for (const skill of skills) {\n    const skillName = skill.name;\n    const skillSourceDir = path.dirname(skill.location);\n    const destPath = path.resolve(resolvedTarget, skillName);\n\n    const relative = path.relative(resolvedTarget, destPath);\n    if (isInvalidSubpath(relative)) {\n      throw new Error('Invalid skill name: Path traversal detected.');\n    }\n\n    const exists = await fs.lstat(destPath).catch(() => null);","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/google-gemini/gemini-cli/blob/5024443c7217464a66e98f80d73172a26440bd8f/packages/cli/src/utils/skillUtils.ts#L232-L268","documentation":"Thrown when the consent callback (`requestConsent`) returns false, signalling the user declined the prompt that lists which skills will be linked into the target directory. This is intentional user-driven cancellation, not a system failure — the installer treats 'no consent' as a stop condition and surfaces it as a thrown error so callers know nothing was written.","triggerScenarios":"The default consent callback was overridden by a caller that prompts the user (CLI confirmation dialog) and the user selected 'no'. Also triggered if a programmatic caller passes a consent function that returns `false` based on policy, or if the consent prompt is auto-denied in a non-interactive environment.","commonSituations":"Running the link command in a CI/automation context where the prompt defaults to decline; a user reviewing the skill list and choosing not to trust one of them; a wrapper script that auto-answers prompts negatively unless a `--yes` flag is passed.","solutions":["Re-run the command and confirm at the prompt if you actually want to install the skills.","Pass a `--yes` / `--force` equivalent flag or supply a consent callback that returns `true` for non-interactive runs.","If the decline was policy-driven, review the skill list, whitelist the trusted source, and retry.","Catch this specific error and treat it as a soft 'cancelled' status rather than a fatal failure in your orchestration code."],"exampleFix":"// before\nconst consent = (skills, dir) => Promise.resolve(false);\nawait linkSkills(source, scope, onLog, consent); // throws\n\n// after\nconst consent = (skills, dir) => Promise.resolve(true);\nawait linkSkills(source, scope, onLog, consent);","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await linkSkills(source, scope, onLog, consent);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Skill linking cancelled by user.') {\n    // treat as soft cancel — exit code 0 or a 'cancelled' UI state\n    process.exitCode = 0;\n    return;\n  }\n  throw e;\n}","preventionTips":["Distinguish user-cancellation from system errors in your orchestrator (unique message or a custom error class).","In non-interactive pipelines, pass a consent callback that returns true to avoid spurious cancellations.","Provide a `--yes` flag at the CLI layer that opts out of the prompt."],"tags":["skills","user-consent","cancellation","ux"],"backgroundTag":null,"analyzedSha":"5024443c7217464a66e98f80d73172a26440bd8f","analyzedAt":"2026-08-12T06:01:53.711Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}