packages/cli/skills/pikku-config/SKILL.md
Use when managing secrets, environment variables, config, or OAuth2 credentials in a Pikku app. Covers wireSecret, wireVariable, wireOAuth2Credential, and typed config access. TRIGGER when: code uses wireSecret/wireVariable/wireOAuth2Credential, user asks about env vars, secrets, config, OAuth2, or "how do I access environment variables". DO NOT TRIGGER when: user asks about API versioning/breaking changes (use pikku-versioning), service factories (use pikku-services), or auth middleware (use pikku-security).
npx skillsauth add pikkujs/pikku pikku-configInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill as an execution checklist, not reference material.
pikku-meta when available; otherwise run the relevant pikku meta ... --json command and inspect only the focused output you need..pikku, node_modules, vendored packages, or broad build artifacts.pikku-verify or pikku all when functions, wirings, schemas, or generated clients may have changed.Manage secrets, variables, and OAuth2 credentials. Never use process.env in Pikku functions — use typed services instead.
pikku info functions --verbose # See existing functions and their versions
pikku info tags --verbose # Understand project organization
See pikku-concepts for the core mental model.
wireSecret(config)Declare a secret with a Zod schema for type-safe access:
wireSecret({
name: string, // Secret identifier
schema: ZodSchema, // Shape and validation
})
wireVariable(config)Declare a variable (non-sensitive config) with a Zod schema:
wireVariable({
name: string,
schema: ZodSchema,
})
// Secrets — encrypted, sensitive values
const config = await services.secrets.getSecretJSON('SECRET_NAME')
// Variables — plain-text configuration
const flags = await services.variables.getVariableJSON('VARIABLE_NAME')
// Simple string access
const apiKey = services.variables.get('API_KEY')
import { LocalSecretService, LocalVariablesService } from '@pikku/core/services'
const createSingletonServices = pikkuServices(async (config) => ({
secrets: new LocalSecretService(), // Reads from .env or local files
variables: new LocalVariablesService(), // Reads from environment
}))
// Declare secrets with typed schemas
wireSecret({
name: 'STRIPE_CONFIG',
schema: z.object({
apiKey: z.string().startsWith('sk_'),
webhookSecret: z.string(),
}),
})
// In your function — fully typed
const config = await secrets.getSecretJSON('STRIPE_CONFIG')
// config.apiKey → string (autocompleted)
// config.webhookSecret → string (autocompleted)
// Declare variables
wireVariable({
name: 'FEATURE_FLAGS',
schema: z.object({
darkMode: z.boolean(),
maxUploadMB: z.number().default(10),
}),
})
// Read it — typed and validated
const flags = await variables.getVariableJSON('FEATURE_FLAGS')
// flags.darkMode → boolean
// flags.maxUploadMB → number
wireOAuth2Credential(config)wireOAuth2Credential({
name: string, // Credential identifier
displayName: string, // Human-readable name
secretId: string, // Secret holding { clientId, clientSecret }
tokenSecretId: string, // Secret for token storage (auto-refreshed)
authorizationUrl: string, // OAuth2 authorization endpoint
tokenUrl: string, // OAuth2 token endpoint
scopes: string[], // Required OAuth2 scopes
})
wireOAuth2Credential({
name: 'slackOAuth',
displayName: 'Slack OAuth',
secretId: 'SLACK_OAUTH_APP',
tokenSecretId: 'SLACK_OAUTH_TOKENS',
authorizationUrl: 'https://slack.com/oauth/v2/authorize',
tokenUrl: 'https://slack.com/api/oauth.v2.access',
scopes: ['chat:write', 'channels:read'],
})
// In your function — tokens refresh automatically
const response = await slackOAuth.request(
'https://slack.com/api/chat.postMessage',
{
method: 'POST',
body: JSON.stringify({ channel, text }),
}
)
const data = await response.json()
Never use process.env inside Pikku functions. Use the variables or secrets service:
// ❌ Wrong
const apiKey = process.env.API_KEY
// ✅ Correct
const apiKey = services.variables.get('API_KEY')
process.env belongs only in server bootstrap code (start.ts).
// schemas/config.ts
wireSecret({
name: 'DATABASE_CONFIG',
schema: z.object({
connectionString: z.string().url(),
maxPoolSize: z.number().default(10),
}),
})
wireVariable({
name: 'APP_CONFIG',
schema: z.object({
appName: z.string(),
maxUploadSizeMB: z.number().default(10),
maintenanceMode: z.boolean().default(false),
}),
})
wireOAuth2Credential({
name: 'githubOAuth',
displayName: 'GitHub OAuth',
secretId: 'GITHUB_OAUTH_APP',
tokenSecretId: 'GITHUB_OAUTH_TOKENS',
authorizationUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
scopes: ['read:user', 'repo'],
})
// functions/admin.functions.ts
export const getAppStatus = pikkuSessionlessFunc({
title: 'Get App Status',
func: async ({ variables, secrets }) => {
const appConfig = await variables.getVariableJSON('APP_CONFIG')
return {
appName: appConfig.appName,
maintenanceMode: appConfig.maintenanceMode,
}
},
})
documentation
Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `pikku create`, `git clone <template>`, or the user says "I cloned the kanban template / starter / template"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.
development
Make a Pikku frontend work in both English (LTR) and Arabic / right-to-left languages. Direction is derived from the active locale, applied once at the document root, and the layout mirrors itself — but only if styling is written flow-relative (margin-inline-start, text-align: start, Mantine ms/me) instead of left/right. TRIGGER when: adding Arabic (or Hebrew/Farsi/Urdu), asked to "support RTL / right-to-left / bidi / mirror the layout", or writing layout styles in an app that may run RTL. Builds on pikku-i18n (an RTL language is just another locale file). DO NOT TRIGGER for backend functions or for LTR-only copy changes.
development
Wire i18n into a Pikku frontend (Vite SPA, Vite SSR, or Next.js app-router) with react-i18next + i18next. English by default, every user-facing string goes through a `t()` token, and additional languages are served under `/de` `/es` URL prefixes. TRIGGER when: scaffolding or editing a frontend and writing user-facing text, adding a second language, or asked to "make this translatable / use tokens / add i18n". DO NOT TRIGGER for backend functions, error messages thrown from functions, or log output.
development
Use when translating an n8n Code node body into a real Pikku function body. Triggered when the user opens or points at a stub generated by @pikku/n8n-import (look for `STUB — generated from n8n Code node` in the file's JSDoc), or when the user says 'translate this n8n code', 'port this n8n code node', 'finish the codeStub__... function', etc. The stub file is a `pikkuSessionlessFunc` with a Zod input/output, a JSDoc preserving the original n8n JavaScript verbatim, and a `throw new Error('… — implement me')` body.