{"record":{"id":"6a10e00689d95ba1","repo":"paperclipai/paperclip","slug":"failed-to-create-api-key","errorCode":null,"errorMessage":"Failed to create API key","messagePattern":"Failed to create API key","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/src/commands/client/agent.ts","lineNumber":797,"sourceCode":"        \"--no-install-skills\",\n        \"Skip installing Paperclip skills into ~/.codex/skills, ~/.claude/skills, and ~/.kimi-code/skills\",\n      )\n      .action(async (agentRef: string, opts: AgentLocalCliOptions) => {\n        try {\n          const ctx = resolveCommandContext(opts, { requireCompany: true });\n          const query = new URLSearchParams({ companyId: ctx.companyId ?? \"\" });\n          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","sourceCodeStart":779,"sourceCodeEnd":815,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/cli/src/commands/client/agent.ts#L779-L815","documentation":"HTTP 404 with body {\"error\":\"Routine trigger not found\"} from POST /api/routine-triggers/:id/rotate-secret, first guard: svc.getTrigger(req.params.id) returned null - no trigger exists with the given ID. Rotation of a webhook trigger's secret cannot proceed on a trigger that was deleted or never existed; the check runs before company scoping.","triggerScenarios":"Rotating the secret of a trigger deleted by another admin; rotating after the routine (and its triggers) were recreated from a revision, leaving the client holding the old trigger ID; a malformed ID in the rotate call.","commonSituations":"Key-rotation cron jobs that iterate a stored trigger inventory without refreshing it; incident response rotating all webhook secrets while a concurrent cleanup deletes unused triggers.","solutions":["Refresh the trigger list from the server before rotating; rotate only IDs that still appear.","On 404, remove the ID from the rotation inventory (it can never come back) and continue with the rest.","Create a replacement trigger via POST /api/routines/:id/triggers if the deleted trigger's function is still needed, then rotate its secret at creation time.","Log 404s during rotation runs and alert if a large fraction disappear (signals environment drift)."],"exampleFix":"// before\nfor (const id of storedTriggerIds) {\n  await api.post(`/api/routine-triggers/${id}/rotate-secret`, {});\n}\n\n// after\nfor (const id of storedTriggerIds) {\n  const res = await api.post(`/api/routine-triggers/${id}/rotate-secret`, {});\n  if (res.status === 404) {\n    retiredTriggerIds.add(id); // drop from inventory; trigger is gone\n    continue;\n  }\n}","handlingStrategy":"validation","validationCode":"async function buildRotationRoster(api: ApiClient): Promise<string[]> {\n  // derive trigger IDs fresh each cycle so deleted triggers drop out automatically\n  const routines = await api.list('/api/routines');\n  const ids: string[] = [];\n  for (const r of routines) {\n    ids.push(...(r.triggers ?? []).map((t: { id: string }) => t.id));\n  }\n  return ids;\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}\nconst isTriggerNotFound = (b: unknown): boolean => isApiErrorBody(b) && b.error === 'Routine trigger not found';","tryCatchPattern":"for (const id of roster) {\n  try {\n    await api.post(`/api/routine-triggers/${id}/rotate-secret`, {});\n  } catch (err) {\n    if (err instanceof ApiError && err.status === 404 && isTriggerNotFound(err.body)) {\n      retired.add(id); // trigger gone; nothing to rotate\n      continue;\n    }\n    throw err;\n  }\n}","preventionTips":["Rebuild the rotation roster from live listings every run; never store it durably.","On 404, retire the ID immediately - trigger IDs are never resurrected.","Update webhook receivers with new secrets only after a 200 from rotation; 404 means old secret still applies.","Alert when a large share of the roster 404s - it signals environment drift."],"tags":["http-404","express","routines","triggers","secrets","rotation","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"}