diff --git a/src/commands/cli/setup.ts b/src/commands/cli/setup.ts index 5e264d5b2..00c0ff748 100644 --- a/src/commands/cli/setup.ts +++ b/src/commands/cli/setup.ts @@ -8,6 +8,7 @@ import { existsSync, unlinkSync } from "node:fs"; import { dirname, join } from "node:path"; +import { captureException } from "@sentry/node-core/light"; import type { SentryContext } from "../../context.js"; import { installAgentSkills } from "../../lib/agent-skills.js"; import { @@ -331,6 +332,10 @@ async function bestEffort( await fn(); } catch (error) { warn(stepName, error); + captureException(error, { + level: "warning", + tags: { "setup.step": stepName }, + }); } } diff --git a/src/lib/agent-skills.ts b/src/lib/agent-skills.ts index 1a85f4f49..48fd4a7c4 100644 --- a/src/lib/agent-skills.ts +++ b/src/lib/agent-skills.ts @@ -8,8 +8,9 @@ * produced by script/generate-skill.ts), so no network fetch is needed. */ -import { existsSync, mkdirSync } from "node:fs"; +import { accessSync, constants, existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; +import { captureException } from "@sentry/node-core/light"; import { SKILL_FILES } from "../generated/skill-content.js"; /** Where skills are installed */ @@ -62,6 +63,16 @@ export async function installAgentSkills( return null; } + // Verify .claude is writable before attempting file creation. + // In sandboxed environments (e.g., Claude Code sandbox), .claude may exist + // but be read-only. Some sandboxes terminate the process on write attempts, + // bypassing JavaScript error handling — so we must check before writing. + try { + accessSync(join(homeDir, ".claude"), constants.W_OK); + } catch { + return null; + } + try { const skillPath = getSkillInstallPath(homeDir); const skillDir = dirname(skillPath); @@ -86,7 +97,11 @@ export async function installAgentSkills( created: !alreadyExists, referenceCount, }; - } catch { + } catch (error) { + captureException(error, { + level: "warning", + tags: { "setup.step": "agent-skills" }, + }); return null; } } diff --git a/test/lib/agent-skills.test.ts b/test/lib/agent-skills.test.ts index fc78fcb45..d4135d3ce 100644 --- a/test/lib/agent-skills.test.ts +++ b/test/lib/agent-skills.test.ts @@ -129,5 +129,20 @@ describe("agent-skills", () => { // Restore write permission so afterEach cleanup can remove it chmodSync(join(testDir, ".claude"), 0o755); }); + + test("returns null when .claude exists but is not writable (sandbox)", async () => { + // Simulate a sandboxed .claude directory: readable + executable, not writable. + // The accessSync(W_OK) pre-check should catch this before any write attempt. + mkdirSync(join(testDir, ".claude"), { recursive: true, mode: 0o555 }); + + const result = await installAgentSkills(testDir); + expect(result).toBeNull(); + + // Verify no write was attempted — skills subdir should not exist + expect(existsSync(join(testDir, ".claude", "skills"))).toBe(false); + + // Restore write permission so afterEach cleanup can remove it + chmodSync(join(testDir, ".claude"), 0o755); + }); }); });