{"record":{"id":"4e44edfac662b307","repo":"paperclipai/paperclip","slug":"could-not-locate-local-paperclip-skills-directory","errorCode":null,"errorMessage":"Could not locate local Paperclip skills directory. Expected ./skills in the repo checkout.","messagePattern":"Could not locate local Paperclip skills directory\\. Expected \\./skills in the repo checkout\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/src/commands/client/agent.ts","lineNumber":804,"sourceCode":"          const agentRow = await ctx.api.get<Agent>(\n            `${apiPath`/api/agents/${agentRef}`}?${query.toString()}`,\n          );\n          if (!agentRow) {\n            throw new Error(`Agent not found: ${agentRef}`);\n          }\n\n          const now = new Date().toISOString().replaceAll(\":\", \"-\");\n          const keyName = opts.keyName?.trim() ? opts.keyName.trim() : `local-cli-${now}`;\n          const key = await ctx.api.post<CreatedAgentKey>(apiPath`/api/agents/${agentRow.id}/keys`, { name: keyName });\n          if (!key) {\n            throw new Error(\"Failed to create API key\");\n          }\n\n          const installSummaries: SkillsInstallSummary[] = [];\n          if (opts.installSkills !== false) {\n            const skillsDir = await resolvePaperclipSkillsDir(__moduleDir, [path.resolve(process.cwd(), \"skills\")]);\n            if (!skillsDir) {\n              throw new Error(\n                \"Could not locate local Paperclip skills directory. Expected ./skills in the repo checkout.\",\n              );\n            }\n\n            installSummaries.push(\n              await installSkillsForTarget(skillsDir, codexSkillsHome(), \"codex\"),\n              await installSkillsForTarget(skillsDir, claudeSkillsHome(), \"claude\"),\n              await installSkillsForTarget(skillsDir, kimiSkillsHome(), \"kimi\"),\n            );\n          }\n\n          const exportsText = buildAgentEnvExports({\n            apiBase: ctx.api.apiBase,\n            companyId: agentRow.companyId,\n            agentId: agentRow.id,\n            apiKey: key.token,\n          });\n","sourceCodeStart":786,"sourceCodeEnd":822,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/cli/src/commands/client/agent.ts#L786-L822","documentation":"HTTP 404 with body {\"error\":\"Routine trigger not found\"} from POST /api/routine-triggers/:id/rotate-secret, second guard: the trigger row exists, but assertCanManageExistingRoutine(req, trigger.routineId) returned null - the parent routine is missing or the caller has no company access to it (routines.ts:108-118). The shared message deliberately hides whether the trigger or the routine access failed, blinding cross-tenant enumeration.","triggerScenarios":"Trigger found but its routine deleted (orphaned trigger); rotating secrets on another company's trigger with a mismatched API key; agent key whose companyId differs from routine.companyId.","commonSituations":"Global secret-rotation tooling using one key across companies; triggers left dangling after routine removal; restored databases where trigger rows outlive their routines.","solutions":["Verify the parent routine: GET /api/routines/:routineId must return 200 with the same credentials used for rotation.","Use company-matched credentials for each rotation batch.","Skip and report orphaned triggers (parent missing) as data hygiene issues instead of retrying rotation.","Remember webhook receivers must be updated with the new secret only after a successful rotation; a 404 means the old secret is still the effective one."],"exampleFix":"// before\nawait api.post(`/api/routine-triggers/${triggerId}/rotate-secret`, {});\n\n// after\nconst routine = await api.get(`/api/routines/${trigger.routineId}`);\nif (!routine) {\n  logger.warn(`trigger ${triggerId} parent routine inaccessible; skipping rotation`);\n  return null;\n}\nreturn api.post(`/api/routine-triggers/${triggerId}/rotate-secret`, {});","handlingStrategy":"validation","validationCode":"async function rotateIfParentAccessible(api: ApiClient, trigger: { id: string; routineId: string }) {\n  const parent = await api.fetch(`/api/routines/${trigger.routineId}`);\n  if (parent.status === 404) {\n    return { skipped: true, reason: `parent routine ${trigger.routineId} missing or cross-company` };\n  }\n  return api.fetch(`/api/routine-triggers/${trigger.id}/rotate-secret`, { method: 'POST' });\n}","typeGuard":"function isApiErrorBody(body: unknown): body is { error: string } {\n  return typeof body === 'object' && body !== null &&\n    typeof (body as Record<string, unknown>).error === 'string';\n}","tryCatchPattern":"try {\n  await api.post(`/api/routine-triggers/${triggerId}/rotate-secret`, {});\n} catch (err) {\n  if (err instanceof ApiError && err.status === 404 && err.body?.error === 'Routine trigger not found') {\n    const parent = await api.get(`/api/routines/${routineId}`);\n    if (!parent) { // inaccessible parent: credential/tenant issue, not retryable\n      throw new Error('cannot rotate: parent routine not accessible with current key');\n    }\n    return; // trigger itself gone\n  }\n  throw err;\n}","preventionTips":["Scope each rotation batch to one company's credentials.","Skip and flag orphaned triggers (parent 404) for maintenance instead of retrying.","Never assume rotation succeeded on 404 - receivers must keep the old secret until a 200.","Audit routine deletions that leave trigger rows behind."],"tags":["http-404","express","routines","triggers","secrets","rotation","tenant-isolation","paperclip"],"backgroundTag":"http-404-resource-not-found","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-18T22:49:45.177Z","contentChangedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}