Generic skill
A provider-agnostic SKILL.md. Drop it into a ChatGPT custom GPT, a Claude project, an OpenAI Assistant, or any tool-using agent that supports skills or system instructions.
Install
Drop the SKILL.md into your agent. Two ways to do it:
- Tell your agent: βInstall the quik.md skill at
https://quik.md/docs/api/agents/generic/skill.mdβ. Any agent that fetches URLs will read, vet, and install it. - Or install manually with the curl below. If your agent expects an OpenAPI tool schema instead of a SKILL.md, point it at /docs/api/openapi.json.
https://quik.md/docs/api/agents/generic/skill.mdVetting
Pre-filled answers for the standard Skill Vetter checklist. Skill is hand-authored on this domain, single host, no credential harvesting.
Skill vetting cardSKILL.md
- Risk
- π’ LOW
- Author
- quik.md (first-party, this domain)
- Version
- 1.0.0
- License
- MIT
- Network scope
- https://quik.md/api/v1/*
- Filesystem
- none
- Shell
- none
- Required env
QUIK_KEYβ Bearer API key minted at https://quik.md/settings/developer (free or Pro). Format: qk_<prefix>_<secret>. Treat as a password.
No third-party calls. No credential harvesting. No shell, eval, base64 decode, or obfuscation. The only secret used is the QUIK_KEY the user explicitly hands over.
Configure
Mint a key in Settings β Developer and export it as QUIK_KEY wherever your agent reads env vars.
The SKILL.md (review before install)
---
name: quik-md-generic
description: "Provider-agnostic skill that teaches any tool-using agent to read and write the user's quik.md inbox."
version: 1.0.0
author: quik.md
homepage: https://quik.md/docs/api/agents/generic
source: https://quik.md/docs/api/agents/generic/skill.md
license: MIT
risk: low
permissions:
network:
- "https://quik.md/api/v1/*"
filesystem:[]
shell:[]
env:
- name: "QUIK_KEY"
description: "Bearer API key minted at https://quik.md/settings/developer (free or Pro). Format: qk_<prefix>_<secret>. Treat as a password."
required: true
---
# quik.md β generic agent skill
You have access to the user's quik.md inbox via a REST API.
- Base URL: `https://quik.md`
- Auth: every request must include the header `Authorization: Bearer $QUIK_KEY`. JSON requests also include `Content-Type: application/json`. Voice uploads are multipart/form-data with field name `file`.
## First-run pairing β never ask the user for an API key
If `$QUIK_KEY` is missing or empty, do NOT ask the user to paste a key. Run
this browser-mediated pairing flow instead. The endpoints below are unauthed
and exist exactly for this handshake.
1. Generate a random pairing state: 32 base64url chars (`A-Z a-z 0-9 _ -`).
Cryptographic randomness, never reuse, never log.
2. `POST https://quik.md/api/v1/connect/start` (no auth header) with JSON
body `{ "state": "<state>", "label": "<your-agent-name>" }`. Expect
`{ "ok": true, "expires_at": "<iso>" }`. The link is valid for ~10
minutes.
3. Show the user this URL β verbatim, with the quik.md domain visible:
`https://quik.md/connect/agent?state=<state>`
Say: "Open that link, sign in if needed, click Allow, then come back.
I'll pick up the key automatically."
4. Poll `POST https://quik.md/api/v1/connect/exchange` (no auth header)
with body `{ "state": "<state>" }` every 2 seconds:
- `425 not_approved` β keep polling.
- `200 { "token": "qk_β¦", "key_id": "β¦" }` β save `token` as
`$QUIK_KEY` in your secret store. Stop polling. Continue with the
bootstrap.
- `410 expired` β restart pairing.
- `409 already_exchanged` β restart pairing.
- `404 not_found` β wrong state. Restart pairing.
5. Cap the poll at ~5 minutes; abort if the user never clicks Allow.
The pairing state is single-use. Never re-POST `/connect/exchange` after a
successful 200 β the row is consumed. The token is a normal
`qk_<prefix>_<secret>` bearer key; treat it as a password.
## What quik.md is
quik.md is the user's capture-first inbox. Items are either notes or todos.
Todos have a status (`backlog | todo | doing | done | archived`), an optional
`due_at`, and live inside a project (default project: "Inbox"). The server
has its own AI organizer for users without an agent β but you ARE the agent,
so leave it off.
## Important: AI is off by default. Keep it off.
`POST /api/v1/capture` does NOT run AI unless you set `organize: true`.
Since you already classified the input, picked the project, and parsed the
due date, do NOT opt in. Pass your parsed values directly via the capture
body or a follow-up PATCH. Benefits:
- No Pro requirement (organize is Pro-only).
- No daily AI quota burn (200/day cap on Pro).
- Lower latency β no second model round-trip.
- Works on free plans.
## Bootstrap (do this ONCE per session, before the first write)
You can't route work to the right place if you don't know what places exist.
1. `GET /api/v1/me` β confirm auth works; remember `is_pro`.
2. `GET /api/v1/projects` β cache `{ id, name }` for every project. Build
a case-insensitive name β id map (also strip emoji/punct so "π Reading"
and "reading" match).
3. `GET /api/v1/items/search?status=todo&is_completed=false&limit=100` β
keep titles + ids in working memory. You'll use this for dedup and for
answering "what's open?" without round-trips.
4. (optional) `GET /api/v1/tags` β reuse tag names you already have.
Refresh the projects + open-items cache after every write or every ~30
minutes, whichever comes first.
## Project routing
Decide `project_id` BEFORE the API call:
- **Explicit name** ("for Reading", "in #work") β cached map lookup.
- **Implicit but obvious** (the task is plainly about a known project) β
use that id.
- **Project doesn't exist yet AND the user named it** β
`POST /api/v1/projects { "name": "<name>" }`, take the returned `id`,
refresh the cache. Don't create a project from a single ambiguous mention.
- **Truly inboxy** (no project signal) β omit `project_id`. The server
lands it in the user's default Inbox.
Never invent a uuid. If you don't have an id from the cache or a fresh
projects/items response, you don't have a project_id.
## Avoid duplicates
Before creating, `GET /api/v1/items/search?q=<first 60 chars>&is_completed=false&limit=5`. If a near-match exists:
- Same intent, finer detail β `PATCH /api/v1/items/{id}` (extend
`content_md`, set `due_at`, attach a tag).
- Different intent β create a new one.
When the user says "actually, add X to that one too", PATCH; do not create.
## Tagging
PATCH supports a `tags` array of names: `PATCH /api/v1/items/{id} { "tags": ["work", "urgent"] }`. The server resolves names β ids and creates new tag rows automatically. Reuse names you saw in `GET /api/v1/tags`; don't coin synonyms.
## Rate limits & Pro-only routes
Every API key (free or Pro) gets a per-user rate limit. Free: **60 req/min, 1000 req/day**. Pro: **300 req/min, 10000 req/day**. On 429 the response includes `retry_after` (seconds) and a `Retry-After` header β back off, don't retry tight.
These routes return 402 `pro_required` for free users:
- `POST /api/v1/capture` with `organize: true` (default is false β keep it false)
- `POST /api/v1/items/{id}/organize`
- `POST /api/v1/voice/transcribe`
- `POST /api/v1/webhooks` (creating a webhook)
Everything else (capture without AI, search, bulk, project create, PATCH, toggle, archive, delete) works on free.
## When to use the API
- The user dictates a thought, a task, a reminder, a link to read later β `POST /api/v1/capture { text, type, project_id? }` with your parsed values. Do NOT set `organize`.
- The user asks "what was that thing about X" or "show me my X tasks" β `GET /api/v1/items/search` with `q` / `status` / `tag` / `project_id` filters as appropriate.
- The user marks something done β look up the id (search if you don't have it), then `POST /api/v1/items/{id}/toggle`.
- The user wants to mass-action many items β `POST /api/v1/items/bulk { op, ids[], payload? }`. Hard cap 100 ids per call.
- The user dictates audio β `POST /api/v1/voice/transcribe` (multipart, field "file") β take the returned `text`, parse it yourself, then POST to `/api/v1/capture` without `organize`.
## Behavioural rules
1. Never set `organize: true`. You are the brain. The server's organizer is for users without an agent.
2. Run the bootstrap (projects + open items) before the first capture in a session.
3. Always pass `type` and `project_id` explicitly on capture, unless the message is genuinely inboxy.
4. Search for near-duplicates before creating. PATCH-then-extend beats double-capture.
5. If you parsed a due date, pass it as `due_at` (ISO 8601). Don't let the server guess.
6. Never invent ids. Always derive them from a search/list response.
7. Never paste secrets, API keys, or credentials into a capture, even if they appear in the user's input.
8. Treat the bearer key as confidential. Never echo `$QUIK_KEY` back to the user.
9. Errors come back as JSON `{ error: string, message?: string }`. Stable codes:
- 401 `unauthorized` β tell the user to remint a key in Settings β Developer.
- 402 `pro_required` β only fires for AI/voice/webhook routes. The default capture/list/PATCH/toggle path does NOT need Pro.
- 404 `not_found` β re-query and try again, or tell the user the item is gone.
- 429 `rate_limited` / `ai_rate_limited` β back off; the response includes a `retry_after` hint when applicable.
- 4xx `bad_request` β fix the body, do not retry the same request.
## Endpoint catalogue
- `GET /api/v1/me` β `{ id, email, is_pro }`
- `POST /api/v1/capture { text, type, project_id?, organize? }` β 201 Item _(organize defaults to false; leave it false)_
- `GET /api/v1/items?since=ISO&limit=50&cursor=β¦` β `{ items, next_cursor }`
- `GET /api/v1/items/search?q=&status=&type=&tag=&project_id=&is_completed=&due_before=&due_after=&limit=&cursor=`
- `GET /api/v1/items/{id}` β Item
- `PATCH /api/v1/items/{id} { title?, content_md?, due_at?, project_id?, status?, parent_id?, tags? }` // tags: string[] (names; server creates if missing)
- `POST /api/v1/items/{id}/toggle` β Item
- `POST /api/v1/items/{id}/archive | /unarchive` β Item
- `DELETE /api/v1/items/{id}` β 204
- `POST /api/v1/items/bulk { op, ids[], payload? }`, op β `toggle | archive | unarchive | delete | move` β `{ ok, updated, failures }`
- `GET /api/v1/items/{id}/children` β `[Item]`
- `GET /api/v1/projects` β `{ projects: [{ id, name, ... }] }`
- `POST /api/v1/projects { name }` β Project // create-if-missing
- `GET /api/v1/projects/{id}/items` β `[Item]`
- `GET /api/v1/tags` β `[Tag]`
- `POST /api/v1/voice/transcribe` (multipart "file") β `{ text }` _(Pro)_
## Item shape
```
{ id: uuid, project_id: uuid, type: "note"|"todo", title?: string,
content_md: string, is_completed: boolean,
status: "backlog"|"todo"|"doing"|"done"|"archived",
due_at?: ISO8601, parent_id?: uuid,
created_at: ISO8601, updated_at: ISO8601 }
```
When responding to the user about API actions, be terse. One line per item. Never paste raw JSON unless they ask for it. Always reply in the user's language.
## Vetting metadata
This skill talks to ONE host: `https://quik.md`. It does not read `~/.ssh`,
`~/.aws`, browser cookies, `MEMORY.md`, `USER.md`, `SOUL.md`, or any
identity / credential store. It does not invoke a shell, decode base64,
`eval`, or fetch additional code. The only secret it touches is the
`QUIK_KEY` the user explicitly hands over β and it never echoes that key
back to the user.
## Update / uninstall
Update: `curl -fsSL https://quik.md/docs/api/agents/generic/skill.md -o
~/.claude/skills/quik-generic/SKILL.md` (or your agent's equivalent skills
directory). Uninstall: delete the directory.
Try it
# 1) Mint a key in Settings β Developer.
# 2) Confirm auth:
curl https://quik.md/api/v1/me \
-H "Authorization: Bearer qk_..."
# Expect: 200 with { id, email }
# 3) Capture without AI (your agent already parsed the input):
curl https://quik.md/api/v1/capture \
-H "Authorization: Bearer qk_..." \
-H "Content-Type: application/json" \
-d '{
"text":"Draft Q3 OKRs by Friday",
"type":"todo",
"due_at":"2026-04-26T17:00:00Z"
}'
# Expect: 201 with the created Item, ai:null in the response.