paperclipai/paperclip · error

Could not locate local Paperclip skills directory. Expected…

Error message

Could not locate local Paperclip skills directory. Expected ./skills in the repo checkout.

What it means

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.

Solutions

  1. Verify the parent routine: GET /api/routines/:routineId must return 200 with the same credentials used for rotation.
  2. Use company-matched credentials for each rotation batch.
  3. Skip and report orphaned triggers (parent missing) as data hygiene issues instead of retrying rotation.
  4. 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.

Example fix

// before
await api.post(`/api/routine-triggers/${triggerId}/rotate-secret`, {});

// after
const routine = await api.get(`/api/routines/${trigger.routineId}`);
if (!routine) {
  logger.warn(`trigger ${triggerId} parent routine inaccessible; skipping rotation`);
  return null;
}
return api.post(`/api/routine-triggers/${triggerId}/rotate-secret`, {});
Defensive patterns

Strategy: validation

Validate before calling

async function rotateIfParentAccessible(api: ApiClient, trigger: { id: string; routineId: string }) {
  const parent = await api.fetch(`/api/routines/${trigger.routineId}`);
  if (parent.status === 404) {
    return { skipped: true, reason: `parent routine ${trigger.routineId} missing or cross-company` };
  }
  return api.fetch(`/api/routine-triggers/${trigger.id}/rotate-secret`, { method: 'POST' });
}

Type guard

function isApiErrorBody(body: unknown): body is { error: string } {
  return typeof body === 'object' && body !== null &&
    typeof (body as Record<string, unknown>).error === 'string';
}

Try / catch

try {
  await api.post(`/api/routine-triggers/${triggerId}/rotate-secret`, {});
} catch (err) {
  if (err instanceof ApiError && err.status === 404 && err.body?.error === 'Routine trigger not found') {
    const parent = await api.get(`/api/routines/${routineId}`);
    if (!parent) { // inaccessible parent: credential/tenant issue, not retryable
      throw new Error('cannot rotate: parent routine not accessible with current key');
    }
    return; // trigger itself gone
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: Global secret-rotation tooling using one key across companies; triggers left dangling after routine removal; restored databases where trigger rows outlive their routines.

Related errors


AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-18). Data as JSON: /api/errors/4e44edfac662b307. Report an issue: GitHub.

Appendix: source

Thrown at cli/src/commands/client/agent.ts:804

          const agentRow = await ctx.api.get<Agent>(
            `${apiPath`/api/agents/${agentRef}`}?${query.toString()}`,
          );
          if (!agentRow) {
            throw new Error(`Agent not found: ${agentRef}`);
          }

          const now = new Date().toISOString().replaceAll(":", "-");
          const keyName = opts.keyName?.trim() ? opts.keyName.trim() : `local-cli-${now}`;
          const key = await ctx.api.post<CreatedAgentKey>(apiPath`/api/agents/${agentRow.id}/keys`, { name: keyName });
          if (!key) {
            throw new Error("Failed to create API key");
          }

          const installSummaries: SkillsInstallSummary[] = [];
          if (opts.installSkills !== false) {
            const skillsDir = await resolvePaperclipSkillsDir(__moduleDir, [path.resolve(process.cwd(), "skills")]);
            if (!skillsDir) {
              throw new Error(
                "Could not locate local Paperclip skills directory. Expected ./skills in the repo checkout.",
              );
            }

            installSummaries.push(
              await installSkillsForTarget(skillsDir, codexSkillsHome(), "codex"),
              await installSkillsForTarget(skillsDir, claudeSkillsHome(), "claude"),
              await installSkillsForTarget(skillsDir, kimiSkillsHome(), "kimi"),
            );
          }

          const exportsText = buildAgentEnvExports({
            apiBase: ctx.api.apiBase,
            companyId: agentRow.companyId,
            agentId: agentRow.id,
            apiKey: key.token,
          });

View on GitHub (pinned to a7e689b3c3)