| name | trek-plugin-dev |
| description | Build, test, sign, and publish plugins for TREK, the self-hosted travel planner (github.com/liketrek/TREK). Covers the trek-plugin.json manifest, the definePlugin server API and ctx object, the sandboxed iframe postMessage bridge for widget/page UIs, permissions and egress rules, the enforced `trek` TREK-version range, local development with trek-plugin-sdk, author signing (keygen/--sign, Ed25519 trust-on-first-use), and publishing to the TREK-Plugins community registry including every CI gate. Use when creating or modifying a TREK plugin, working with trek-plugin-sdk or trek-plugin.json, signing a plugin or handling a signature/key-rotation problem (SIGNATURE_KEY_CHANGED, re-trust, allow-key-change), debugging PERMISSION_DENIED / RESOURCE_FORBIDDEN / TREK_VERSION_INCOMPATIBLE / TREK_VERSION_UNKNOWN / API_VERSION_INCOMPATIBLE or a plugin that will not install or activate on a given TREK version, recovering from a failed publish (rollback, unrelease), or preparing a TREK-Plugins registry entry or PR. |
TREK Plugin Development
Build plugins for TREK, a self-hosted
trip-planning app. A plugin is a directory with a manifest (trek-plugin.json),
a built CommonJS server entry (server/index.js), and — for page/widget/
trip-page types — a static client bundle (client/). TREK runs the server part in an
isolated child process reached only over RPC, and the UI in a sandboxed,
opaque-origin iframe. Distribution happens through the
TREK-Plugins registry: a static
index updated by pull request; plugin code and releases stay in the author's
own public GitHub repo.
Everything is driven by the npm package trek-plugin-sdk (Node >= 18):
definePlugin + types, a permission-enforcing mock host for tests, and the
trek-plugin CLI (npx trek-plugin-sdk <command>).
Reference files — read before working on that area
| Task at hand | Read |
|---|
Writing/editing trek-plugin.json, permissions, egress, settings | references/manifest.md |
Server code: definePlugin, routes, jobs, ctx.*, error codes | references/server-api.md |
| Widget/page UI: iframe sandbox, postMessage bridge, CSP | references/client-bridge.md |
Local dev server, fixtures, unit tests with createMockHost | references/testing.md |
Any trek-plugin CLI command and its flags | references/cli.md |
| Releases, registry entries, CI gates, signing, updates | references/publishing.md |
Golden path
create → dev → status → shot → publish. When you don't know what to do next,
run status: it grades every registry gate that can be answered offline and names
exactly one next command.
# 1. Scaffold (id must be a lowercase slug, 3–40 chars)
npx trek-plugin-sdk create my-widget --type widget # or: page | trip-page | integration
# The scaffold RUNS and PACKS immediately — but it does NOT pass `validate`
# (stub README, no screenshot). That's deliberate; `status` says what's missing.
# 2. Develop: edit trek-plugin.json, server/index.js, client/index.html
cd my-widget
npx trek-plugin-sdk dev # http://localhost:4317 — hot reload, no TREK needed
# Enforces your permissions for real: ctx, hooks, events,
# jobs AND egress. Read the banner — it warns about any
# entry point TREK would silently never call (rule 4).
# 3. Where am I? (never fails — orientation, not a gate)
npx trek-plugin-sdk status # checklist: Manifest / Code / Docs / Release / Repo
# + "next → <one command>"
# 4. The store image the registry requires (needs Playwright — see below)
npx trek-plugin-sdk shot # → docs/screenshot.png, 1600×900, in the themed frame
# 5. Write the README (4 sections, ≥400 chars of prose, a row per permission),
# commit + push, then re-run status until it's green. `validate` is the same
# checks with an exit code, for CI.
# 6. Publish: public GitHub repo (convention: trek-plugin-<id>), then ONE command:
npx trek-plugin-sdk publish --repo you/trek-plugin-my-widget --tag v1.0.0 --sign
# = ① check (every offline registry gate) → ② pack → ③ git tag + GitHub release
# → ④ preflight (the gates that need the release to exist) → ⑤ registry PR.
# If a step-① gate fails, NOTHING is packed, tagged, pushed or released — and if a
# LATER step fails, publish ROLLS BACK the release/tags it created, so either way
# you fix and re-run against the SAME version (--keep-release opts out;
# `unrelease <tag>` cleans up an already-stranded state). Requires git + gh (authed).
# In a TERMINAL, publish OFFERS to sign and creates the key for you — --sign is
# only needed in scripts/CI, which are never prompted. (keygen makes the key by
# hand: ~/.trek-plugin/signing.key — ONCE, ever, for all your plugins. BACK IT UP.)
validate is a real gate now, and pack is not. validate errors on
everything the registry would reject offline — a lucide icon that doesn't exist, a
README missing a section / under 400 chars of prose / with placeholders left / with
an unexplained permission, a screenshot that doesn't resolve to a file on disk, an
egress[] host with no matching http:outbound:<host> permission, name/description/
author outside the registry's length limits. pack refuses only what makes a plugin
unloadable (broken manifest, no server/index.js, a native binary), because packing
is how you install into a local TREK to try it — so a green pack is not a green
validate. See references/cli.md.
Say yes when publish offers to sign. A signature proves the artifact came
from you, not merely that the registry vouched for some bytes — so a compromised
registry can't ship code under your name. In a terminal publish proposes it and
makes the key for you; in scripts/CI, which are never prompted, pass --sign.
Signing late is fine — unsigned → signed at v1.4.0 breaks nobody, because
nothing is pinned until a signed version installs; the SDK retro-signs your
older versions automatically on the first signed update (the registry requires
every version signed once a key is present). The thing you cannot do is
stop (see rule 12), so the only decision that ever really binds is whether
you'll keep the key safe. Back it up and sign.
Update flow: bump version in the manifest, re-pack, new vX.Y.Z tag/release,
then entry --merge onto the existing registry file (newest version first) and
PR it. entry hashes your local plugin.zip — make sure it's the same file
you uploaded as the release asset. There's also a hand-edit path — see "Updating
a published plugin" in references/publishing.md.
Build the UI / store shot with the user, not for them
For a page/widget plugin the look is subjective — don't silently pick it,
and don't just describe it in words. Two rules:
1. Propose choices interactively, with suggestions. Before and while building
the UI and the store image, offer the user concrete options tailored to the
plugin and let them choose (use an interactive prompt — e.g. Claude Code's
question UI — not an assumption). Good dimensions to ask about, each with 2–4
suggestions derived from what the plugin does:
- Accent colour(s) — hues that match the subject (weather → sky blue +
sunset orange; a Japanese-phrase plugin → warm coral).
- Store-shot background — dark & atmospheric (an accent glow) vs. light (a
colourful accent mesh).
- Pattern/texture — waves / dots / grid / none (some texture so it isn't flat
and boring).
- Kicker, tagline, and which feature pills to show.
- Layout — light + dark side by side (shows theme support) vs. a single hero.
2. Show the draft as a screenshot for sign-off — don't ship on a description:
- The plain shot is now a CLI command:
npx trek-plugin-sdk shot boots dev,
renders your UI in the themed /preview frame and writes a 1600×900
docs/screenshot.png (--dark for the dark theme). It needs Playwright, which is
deliberately not an SDK dependency — npm i -D playwright && npx playwright install chromium. An integration has no UI, so shot refuses: screenshot the
TREK surface your plugin changes instead (the notification it sends, the badge it
adds to a place, its settings page).
- For a composed store image — light + dark cards side by side, title, kicker,
feature pills, an accent-driven background — the SDK has no equivalent, so use the
skill's own kit:
assets/store-shot.html (set its CONFIG
from the choices above: glow/mesh · waves/dots/grid) driven by
assets/shot.mjs, which also has --preview for
docs/preview-light.png + docs/preview-dark.png (both themes in one run — what
you show for UI sign-off). Set it up once with assets/setup.sh
(--web-hook for Claude Code web) → npm run preview-shot / npm run shot. See
references/testing.md.
- Either way, open
dev's themed /preview (light/dark/accent toggles) while you
iterate, present the image(s), ask "does this look right?", and iterate. The
approved shot doubles as the store docs/screenshot.png — which is a hard
registry gate, and validate/status now fail if it doesn't resolve to a real
file on disk.
See references/testing.md.
Choosing the plugin type
type | Surfaces | Use for |
|---|
widget | Dashboard card (sidebar slot — glassy auto-height) or a non-interactive boarding-pass hero strip (hero slot, ~110px, desktop-only); plus the scoped planner slots place-detail (gets placeId), day-detail (dayId), and reservation-detail (reservationId) | At-a-glance info (flight status, weather, mascot); a per-place/day/reservation add-on |
page | Own entry in the top navigation → full-page iframe (you own the layout) | A self-contained tool |
trip-page | A tab inside every trip planner, scoped to the open trip (tripId always set); full-frame like page, no dashboard nav. capabilities.tripPage can replace core tabs / set tab position (tab-takeover) | A per-trip tool |
integration | No UI; background routes, jobs, events, plus wired provider hooks (place-detail / trip-warning / table / map-marker / map-layer / route / day-schedule / day-tint / pdf-section / atlas-layer / journal-entry / trip-card / photo / calendar / notification-channel) | Feeding/syncing data; enriching core UI natively |
Note: jobs[] need the jobs:run grant — with it, declared cron jobs run
on their cron schedule (userless); without it they never fire. The persistent
ctx.scheduler (at/in/every/cancel, same grant) adds
restart-surviving one-shot/recurring callbacks into a scheduled handler. To
react to core activity, declare events: events: [{ on, handler }] +
events:subscribe; the handler gets { event, tripId, entity?, entityId?, snapshot? } (snapshot only when you also hold the family's db:read:*), runs
with no user, fire-and-forget (the SDK exports the family catalog as
EVENT_FAMILIES / EVENT_SNAPSHOT_GRANT). All fifteen provider hooks are
wired (place-detail / trip-warning / table / map-marker / map-layer / route /
day-schedule / day-tint / pdf-section / atlas-layer / journal-entry / trip-card /
photo / calendar / notification-channel), plus the GDPR hook:user-data
(deleteUserData/exportUserData, userless, own-db) — so an integration can
inject native UI or honour data-rights with no iframe. See
references/server-api.md.
Critical rules (violating any of these breaks install or CI)
- Never vendor
trek-plugin-sdk. The host makes
require('trek-plugin-sdk') resolve inside the child at runtime. Keep it a
devDependency. Any other runtime dependency must be vendored/bundled —
TREK never runs npm install on a plugin.
- Ship built CommonJS.
package.json carries "type": "commonjs";
server/index.js is plain built JS (.ts and .map files are stripped by
pack). Client files are pre-built static assets.
- Egress trap: the runtime network guard and the iframe CSP are built from
the
http:outbound:<host> permissions, not from egress[] (which TREK
never reads at runtime — it is the consent-screen declaration). A host listed in
egress[] but not granted as http:outbound:<host> used to install, activate,
consent and then be silently blocked at runtime; validate/status now
error on it (code.egress-reachable), and the reverse — a reachable host you
forgot to declare in egress[], i.e. understating your own network reach — is a
warning. Keep both lists identical. Bare http:outbound alone reaches nothing — unless you
set operatorEgress: true, which waives the non-empty-egress[] rule so you
ship an empty egress[] and the admin configures the real hosts at
runtime (for plugins whose egress hosts aren't known up front).
trek-plugin dev runs the same guard (SDK ≥ 1.5.0), so an undeclared host fails
locally instead of after install; private/loopback targets are blocked there too unless
you set TREK_PLUGIN_ALLOW_PRIVATE_EGRESS=on, exactly as on the real host.
- Silent-no-op trap: a hook / event / job you implement but never GRANT is never
called.
hooks.* needs its matching hook:* permission, events: [] needs
events:subscribe, jobs: [] and scheduled need jobs:run, and
deleteUserData/ need . Miss one and TREK does
error — it installs, activates, and simply . No log,
no warning; all you see is "my plugin does nothing." / catch
it (your live in , which the manifest validator never
loads); the automatic check is , which warns at load and
403s if you fire one (SDK ≥ 1.5.0). The lone exception is the notification channel — it's declared in the
manifest (), so does catch a missing
.
Isolation model (what plugin code can rely on)
- Own OS process under Node's permission model; filesystem reads scoped to the
plugin's own code. No
JWT_SECRET, no trek.db, no file writes, no child
processes, no worker threads.
- All host access via the
ctx object; an ungranted capability throws
PERMISSION_DENIED, an unknown method UNKNOWN_METHOD.
- Own data only through
ctx.db (a private SQLite file, requires db:own).
- UI iframe: opaque origin (sandbox without
allow-same-origin), no cookies,
no parent DOM; talks to TREK only via postMessage with target origin '*';
CSP default-src 'none', connect-src limited to granted hosts.
- The raw child↔host IPC channel is sealed before your code loads —
process.send / process.on('message') / disconnect are revoked; ctx is
the only channel in.
- Crash/hang/OOM kills only the plugin's process; TREK keeps running. Watchdog:
RSS 300 MB, 192 MB heap, 30 s
onLoad/route timeouts, 5 crashes/5 min →
auto-disabled (see references/server-api.md).
- Per-plugin RPC rate limit: a token bucket at the
ctx dispatch
boundary (defaults burst 60, 20/s, 16 in-flight; env TREK_PLUGIN_RPC_BURST /
_PER_SEC / _INFLIGHT) throttles a runaway plugin instead of freezing the
single-threaded host.
Instance & ops facts
- Plugin system is on by default; kill switch
TREK_PLUGINS_ENABLED=false
(also accepts 0/off/no). Admin UI: Admin → Plugins (Installed /
Discover). Rescan re-reads the plugins directory and force-refreshes the
remote registry (bypasses the 30-min + GitHub CDN cache, so a just-merged
plugin shows up immediately).
- Sideloading: admins can upload a plugin
.zip/.tar.gz via
Admin → Plugins (drag-drop / Upload). It installs inactive, is flagged
Sideloaded (local:upload, unsigned, unreviewed, no auto-update), and
still needs activation + permission consent; same extract/manifest/native
guards as a registry install; ≤ 50 MB.
- Plugin code lives in
TREK_PLUGINS_DIR (default <data>/plugins), plugin
SQLite data in TREK_PLUGINS_DATA_DIR (default <data>/plugins-data).
Behavior-affecting operator vars: TREK_PLUGIN_MAX_RSS_MB (default 300),
TREK_PLUGIN_ALLOW_PRIVATE_EGRESS=on (lifts the SSRF block on internal
addresses), TREK_PLUGIN_PERMISSIONS=off (weakens the OS fs/child sandbox),
TREK_PLUGIN_REGISTRY_URL (override registry source);
TREK_PLUGINS_DEV_LINK=1 enables the DEV-ONLY dev-link workflow
(link/reload a local build against real data — off by default, never set in
production;
see references/cli.md),
the RPC-limit knobs TREK_PLUGIN_RPC_BURST / _PER_SEC / _INFLIGHT, and the
log rate-limit knobs TREK_PLUGIN_LOG_BURST / _PER_SEC (defaults 50/10).
- Per-plugin activity log: every user can audit what plugins did in their
name at
GET /api/plugin-activity (hash-chained audit trail) — design write
paths knowing each call is user-visible.
- Backups include plugins: TREK backup/restore archives each
plugin's per-plugin SQLite data tree and installed code (staged and swapped
in on next boot), so a restore no longer loses plugin state. Older archives
without them are a no-op.
- Installed plugins must be activated one by one; a version bump that requests
permissions requires the admin to re-approve.
Canonical examples
Two official examples in the TREK repo (plugin-sdk/examples/) — pick the one
that matches your plugin's shape:
koffi — the mascot hero-widget: manifest, membership-checked trip
read, the iframe bridge, hand-built inline SVG UI, and a README that passes the
CI gate. The reference for anything with its own sandboxed UI. Registry entry:
registry/plugins/koffi.json in TREK-Plugins.
trip-doctor — a hooks-only, no-UI integration: it feeds
TREK's own planner surfaces through warningProvider.getWarnings and
placeDetailProvider.getDetails, and pins private notes via ctx.meta behind a
POST /pin route. The reference for the provider-hook + db:meta
pattern — TREK renders everything natively, so there's no CSP/iframe to fight.
(Its README's npx @trek/plugin-sdk … and "signed .trekplugin bundle" are
example prose, not the real CLI — the package is trek-plugin-sdk and
pack emits plugin.zip; signing is a separate sign step. Use the commands
in cli.md.)
The Plugin Cookbook (wiki/Plugin-Cookbook.md) collects copy-paste
recipes for the above plus itinerary writes, meta tagging, ws broadcast, and
the trek:ui design kit — a good first stop when you know the capability but not
the exact call.
Reporting errors in this skill
This skill is documentation verified against TREK's source, but TREK evolves and
gaps remain. If, while using this skill, you hit a claim here that contradicts
what the TREK source or a real TREK instance actually does — or a gap that costs
real time — do NOT just move on. Fill in the block below and hand it to the
user, ready to paste, so it can be fixed for everyone.
Emit it verbatim in a fenced code block, every field filled from what you
actually did, then tell the user: "Copy this and paste it into a new issue at
https://github.com/liketrek/Plugin-Skill/issues/new/choose (pick 📋 Paste
an agent-generated report) — it's already filled in."
## Skill feedback
**Type:** discrepancy | missing-guidance
**Skill file + section:** references/<file>.md → <section>
**What the skill says:** <quote the exact wording, or "n/a — not covered">
**What actually happens / what's missing:** <the correct fact or the gap>
**Evidence:** source-read | real TREK instance | trek-plugin dev | custom harness (no real CSP/sandbox) | inferred (
unconfirmed)
**Citation / repro:** <TREK repo path @ commit/tag, or exact steps>
**TREK version:** <x.y.z or unknown>
**trek-plugin-sdk version:** <x.y.z or unknown>
**OS (if tooling-related):** <or n/a>
**Suggested fix (optional):** <proposed wording>
_Generated by the trek-plugin-dev skill._
Be truthful in the Evidence field — it is the most important line. An
inference is not a confirmed discrepancy; label it inferred (unconfirmed)
and say so out loud to the user. This is load-bearing: several reported "bugs"
have turned out to be test-method artifacts (e.g. an image failing in a bare
harness that has none of TREK's real CSP), not skill errors. Prefer to verify
against the TREK source or a real instance before claiming a discrepancy.
Primary sources