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
- 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.
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
- 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.
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
- Failed to create API key
- Cannot build API path with an empty path segment.
- Challenge secret is required. Pass --token or --token-env.
- Company ID is required. Pass --company-id, set…
- Environment variable
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)