Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Build, package, and publish KiroCrew external apps. Covers app.json manifest, UI components, crons, skills, self-healing install, git publishing, and common pitfalls.
KiroCrew App Development
Guide for building external KiroCrew apps that work reliably across KiroCrew versions without depending on buggy lifecycle hooks or skill scanner behavior.
When to Use
User says "create a kirocrew app", "build an app", "make an app for kirocrew"
User says "package this as an app", "publish this to the app store"
User is building UI pages, crons, or skills that should be distributable
For a working example, look at the bundled builtin apps under
src/kiro_crew/apps/builtins/ — they follow the same manifest, UI, and cron
patterns described here and are a good starting point to copy from.
Read version from app.json via /api/file-read, not from data/config.json (stale install-time value) and not from /api/apps/MY-APP (requires session auth the iframe doesn't have).
// After _configReady resolves and you know the app path:const appJsonPath = '/Users/.../.kiro/crew/apps/my-app/app.json'fetch('/api/file-read?path=' + encodeURIComponent(appJsonPath))
.then(r => r.ok ? r.text() : null)
.then(t => { if (t) APP_VERSION = JSON.parse(t).version })
Derive the path from STATE_PATH (replace workspace/my-app/state.json with apps/my-app/app.json) or hardcode it based on the app name.
Header Layout Pattern
All apps use a consistent header: Icon + Title + badge on the left, last-scan + refresh + version on the right.
Render the same SVG from ui/icon.svg directly in JSX (20x20px, stroke: '#7c3aed')
Use _jsx('svg', {...}) with child _jsx('path', { d: '...' }) elements
Don't fetch the icon file at runtime — inline it for instant render
Background Actions (MANDATORY)
All buttons that trigger agent work MUST run in the background via POST /api/chat?ws=1. NEVER navigate to /chat for automated actions — the user should stay on the app page.
Only use navigate('/chat') for actions that genuinely need human involvement (responding to reviewer comments, complex setup wizards).
UI Rules
Do
Don't
Use inline style={} objects everywhere
Use Tailwind classes
Use 30s polling via setInterval + /api/file-read
Use /api/file-watch SSE (overwrites React state on connect)
Use useNavigate() from @kirocrew/app-sdk for navigation
Use window.location (causes full reload)
Use theme vars for backgrounds/text/borders
Hardcode hex for theme-dependent colors
Use #7c3aed / #e8d5f5 for accent elements
Use blue (var(--accent)) or other accent colors
Read version from app.json via /api/file-read
Read version from data/config.json (stale) or /api/apps/MY-APP (needs session auth)
Run background work via POST /api/chat?ws=1
Navigate to /chat for automated actions
Show "initializing" state when config missing
Show cryptic error messages
Read state from file via /api/file-read?path=...
Use non-existent endpoints like /api/files/read
Available Gateway APIs
Endpoint
Method
Returns
/api/apps/MY-APP
GET
Full manifest JSON — requires session auth (NOT usable from app iframe)
/api/apps/MY-APP/config
GET
JSON from data/config.json (no auth needed, but version is stale)
/api/file-read?path=...
GET
text/plain file content
/api/chat?ws=1
POST
Launches agent work in background slot
/api/file-watch
GET (SSE)
Live file change stream (avoid — see above)
No directory listing API exists. Use a _index.json manifest file if you need to list files.
Available Imports (via shared import map)
react (useState, useEffect, useRef, etc.)
react/jsx-runtime (_jsx, _jsxs, Fragment)
react-dom
@kirocrew/app-sdk — hooks (useAppApi, useAppEvents, useTheme, useAppInfo,
useNavigate, useNotify, useNavBadge, useChatLauncher, useChatSession), the
ChatEmbed / ChatPanel / ChatMessageList components, the transcript's row
registry (defaultMessageRenderers, mergeRenderers, resolveRenderer,
ToolCallPill), and the chat marker protocol (parseOptions,
deriveFollowUpOptions, extractSteeringAcks, stripPartialOptionMarker). The protocol
is React-free, so a worker or a plain function can use it too. The registry is how you add
a transcript row type or replace one instead of hand-rolling a message list — see
docs/app-kit/api-reference.md.
Display metadata (description, tags, author) comes from app.json in the app's own repo (cached 24h).
3. Updates
Updates are automatic via semver diff — bump version in app.json, push to the repo. Users with update-check crons see a banner in the UI.
Update-Check Cron Pattern
{"name":"my-app-update-check","message":"Fetch remote app.json via git archive, compare version to installed. Write result to ~/.kiro/crew/workspace/my-app/update-status.json. Always silent.","every":86400,"silent":true,"persistent_session":false}
Self-Update & Refresh Pattern
Apps should include a version check cron and UI elements for manual refresh and update. ALL actions run in the background — no navigation away from the app.
Update-Check Cron
Add to app.json crons array:
{"name":"my-app-update-check","message":"Check if a newer version of MY-APP is available. READ-ONLY. Steps: (1) Remote version: run `git archive --remote=https://github.com/<org>/my-app main app.json | tar -xO` from $HOME. Parse 'version'. (2) Installed version: read ~/.kiro/crew/apps/my-app/app.json. (3) Compare semver. Write ONLY to ~/.kiro/crew/workspace/my-app/update-status.json: {checked:true, installedVersion, remoteVersion, updateAvailable:bool, checkedAt:ISO}. Silent.","every":86400,"silent":true,"persistent_session":false}
The data/ directory contains runtime config (config.json) with resolved paths and stale version info. Committing it causes version display bugs (showing install-time version) and path issues on other machines.
Git Workflow for Installed Apps
The installed app at ~/.kiro/crew/apps/MY-APP/ IS a git repo with origin pointing to the source repository. You can commit and push directly from there:
cd ~/.kiro/crew/apps/my-app && git add -A && git commit -m "message" && git push origin main
No separate workspace needed. The installed app IS the workspace.
Common Pitfalls
Pitfall
Cause
Fix
"no visual interface" in sidebar
Missing ui.entry or wrong extension
Use .mjs ESM with default export
Skills don't load after install
Scanner only checks flat ~/.kiro/crew/skills/<name>/SKILL.md
Self-heal symlink in cron
UI shows stale data after app.json change
Browser caches manifest API response
Hard refresh
onInstall doesn't run
KiroCrew lifecycle hook bug
Don't depend on it — self-heal instead
SSE overwrites React state
/api/file-watch fires immediately on connect
Use polling instead
Cron spams DMs
No dedup — same condition triggers every cycle
Track last_notified in state
App icon doesn't show
Using icon string field
Use iconUrl pointing to SVG file
Stale app after gateway restart
Gateway reads app.json live but session predates
Open new dashboard session
Version shows "?" or old number
/api/apps/MY-APP needs auth, config.json is stale
Read app.json via /api/file-read
Install artifacts in git
data/, .app_secret, etc tracked
Add to .gitignore, git rm --cached
Buttons navigate away from app
Using navigate('/chat') for automated work
Use POST /api/chat?ws=1 background slots
Refresh/update leaves app page
Using window.__mc_chat_launch + navigate
Background slot + disabled state + timeout
Testing Locally
Place app in any directory
Install: kirocrew app install /path/to/my-app
Restart gateway: kirocrew gateway restart
Open fresh dashboard session
Verify: UI loads, cron runs self-heal, skill appears in /skills list
Versioning Convention
Patch (1.0.x): Bug fixes, wording changes
Minor (1.x.0): New features, new crons, UI additions
Major (x.0.0): Breaking changes to state format, removed features
In-Process Backend for External Apps (CRITICAL — differs from builtins)
External (installed) apps CAN ship a Python backend that runs inside the gateway
process — but the contract DIFFERS from builtins (auto_research etc.):
Manifest: use ONLY backend.hooks ("routes": "backend.routes:register_routes").
Do NOT set the backend.routes base-path string — that field triggers the
STANDALONE-PROCESS proxy, which serves dead stubs that shadow your handlers.
register_routes(ctx) receives an AppContext and MUST return list[AppRoute]
(from kiro_crew.apps.route_registry import AppRoute) with paths RELATIVE to
/api/apps/<name>; {params} land in request.match_info.
Handlers take (request, ctx); gateway state is request.app["state"];
auth-check request.get("user") is not None → else 401.
The builtin pattern (direct app.router.add_get) silently never dispatches for
external apps — the RouteRegistry catch-all (/api/apps/{app_name}/{path:.*})
shadows it.
Backend hook changes need a gateway restart OR an app disable→enable cycle
(runtime deregister + module unload + fresh load). UI files reload without.
Trust: backend code runs UNSANDBOXED with full gateway privileges (SEC-012
warning logged; agent.apps_allow_third_party=false refuses it entirely).
Dev Loop for App UIs
Preferred: dev mode — kirocrew app dev <name> (off: --off). Serves that
app's UI with Cache-Control: no-store and watches its ui/ dir; changes
broadcast app_reload and the dashboard hot-swaps the app in ~1s. The flag
lives in installed.json and toggles live.
To edit in your source tree, symlink the installed UI dir to source:
mv ~/.kiro/crew/apps/<n>/ui ~/.kiro/crew/apps/<n>/ui.bak && ln -s <src>/ui ~/.kiro/crew/apps/<n>/ui
(serving containment check and the dev watcher both follow symlinks).
On native Windows use a directory junction instead (PowerShell):
Rename-Item "$env:USERPROFILE\.kiro\crew\apps\<n>\ui" ui.bak; New-Item -ItemType Junction -Path "$env:USERPROFILE\.kiro\crew\apps\<n>\ui" -Target "<src>\ui"
(pathlib resolves junctions the same way, so serving and the watcher work;
the lifecycle clobber below applies identically — junctions are also never
preserved by the install/update safe-copy).
⚠️ Symlinks do NOT survive the app lifecycle.install_app/update_app
(reinstall, App Store Update, registry refresh) re-copy source over the
installed dir with a DELIBERATE symlink-stripping safe-copy (security: blocks
ui -> ~/.docker style serving). Your symlink is silently replaced by a
frozen snapshot: hot reload stops, UI goes stale, no error. Symptom:
ls -l ~/.kiro/crew/apps/<n>/ui shows a real dir, not a link. Fix: re-create
the symlink after ANY install/update, and re-check dev mode is still on.
Same clobber applies to locally-edited SHIPPED skills: installed skill files
under ~/.kiro/crew/skills/ re-sync from the KiroCrew package on update —
durable skill changes must land in the repo (skills/ in the KiroCrew source).
Validate .mjs before relying on a reload: node --check ui/index.mjs —
a parse error surfaces only as "Failed to load : Unexpected token".
Avoid deep _jsx nesting in one expression; prefer small named components.
Dark mode: never pair a solid light accent bg with hardcoded dark text for
selected states — use a translucent accent tint (e.g. rgba(124,58,237,.14))
with var(--text)/var(--muted). Self-contained pills (own bg+fg) are fine.
Don't Reinvent the Dashboard (default posture)
By DEFAULT, apps should look and behave like the dashboard they live in:
Theme tokens over hardcoded colors: var(--accent), var(--accent-fg),
var(--accent-subtle), var(--danger)/var(--danger-subtle), var(--ok),
var(--bg)/var(--card)/var(--border)/var(--text)/var(--muted).
Hardcoded hex breaks the moment a user picks a custom palette (and error
banners hardcoded for light mode glow in dark mode). Give tokens fallbacks
(var(--accent, #7c3aed)) so old hosts still render.
Host components over hand-rolled ones: the @kirocrew/ui module-map
export ships Btn, Input, SearchInput, Badge, Toggle, EmptyState, Skeleton, ContentSkeleton, PageHeader, SegmentedControl, MarkdownRenderer and more;
lucide-react ships a subset of real icons. Feature-detect
(window.__kirocrew_modules?.['@kirocrew/ui']) and keep a small fallback for
old hosts — a thin wrapper per component (host when available, fallback
otherwise) keeps call sites clean.
This is the default, not a straitjacket: if your app has a deliberate,
preferred custom style or a novel interaction with no host equivalent (bespoke
visualizations, a branded look, domain-specific widgets), a custom design is a
legitimate choice — make it consciously and consistently, not as an accident of
copy-pasted inline styles. Custom visuals should still respect the theme's
background/text tokens so they don't break light/dark/custom palettes.
Embedded Chat (ChatEmbed) — native chat inside your app
The host SDK ships the dashboard's real chat renderer. Use it instead of
hand-rolling a transcript view — markdown, tool activity, streaming, and turn
grouping come for free and stay consistent with the main chat.
Access: const sdk = window.__kirocrew_modules?.['@kirocrew/app-sdk'], then
render sdk.ChatEmbed with { slotKey, agent?, placeholder? }. Feature-detect
and keep a lightweight fallback — the module map can lag one gateway version.
slotKey binds the embed to a chat slot (<app-name>-<entity> is the
convention). The embed polls /api/chat/slots/<key> (1s while running, 5s
idle) and POSTs to /api/chat.
Manifest permissions (silent-failure trap): the SDK gates fetches by the
app's permissions.api allowlist. ChatEmbed needs "/api/chat" +
"/api/chat/*"; if your users click Approve/Trust on tool cards you ALSO need
"/api/approvals" + "/api/approvals/*" — without it the button 403s with no
visible error.
Chrome and scroll are props, not CSS overrides. Pass frameless to drop the
bordered card, title strip and input-row border so the embed sits flush inside
your own card, and startAtBottom to jump to the newest turn immediately and
stay pinned there (released when the user scrolls up more than 40px, re-pinned
when they return). Do NOT reach for !important overrides on the embed's
Tailwind classes or hand-roll a scroll keeper: those couple you to host DOM
internals the repo has never promised.
Still missing — tracked in issue #510: permission cards do not render
inside the embed, so a worker slot your app owns cannot ask the user to
approve a tool from there. That is why such slots tend to be blanket-trusted;
treat the trust level as a deliberate decision, not a default.
Rendering agent messages yourself instead of using ChatEmbed? An agent puts
follow-up choices and steer acknowledgements inline in its prose
([OPTIONS: a | b], [STEERING steer-<id>: …]). Parse them with the SDK's
marker protocol rather than by hand, and remember the rule that costs users
their input: stripping a marker WITHOUT offering the affordance deletes the
choices outright — worse than showing the raw text.
Worker Slots — apps that own agent sessions
Stopgap — tracked in issue #509 (a supported acquire_worker_slot(app, project, trust=…) helper): these are underscore-private slot internals, not a
promised API. Until #509 lands they are the only mechanism, but treat this
recipe as scaffolding — re-check it against the SDK when you update an app.
If your app creates chat slots for background/worker agents (spec writers,
researchers), stamp these attributes — and re-stamp on EVERY acquisition, not
just creation, because gateway restarts and other code paths (e.g. ChatEmbed's
own POST) can recreate slots without them:
slot._app = "<app-name>" — keeps the session out of the main chat sidebar.
Trust — grant it BOUNDED, never blanket-forever. Approval prompts render
ONLY in the main chat UI, so an untrusted worker inside an app embed stalls
silently on its first shell command — but the fix is a scoped grant, not a
permanent one:
Preferred: pattern-scoped trust via slot._trusted_patterns (supported
by chat_runner) — allowlist only the tool/command shapes your worker
actually needs.
If you must use blanket slot._trust = True: time-box it. Mirror the
in-repo precedent (auto_research: 24h TTL, then trust expires and
re-authorization re-grants it) rather than re-stamping True
unconditionally forever. A permanent unscoped auto-approve worker silently
exempts a growing class of sessions from the interactive-approval layer —
a security regression that compounds as apps adopt the pattern.
Always SEL-audit the grant, whichever form it takes.
slot.project = <working_dir> — sets the CLI process cwd (chat_runner runs
cwd=slot.project). Without it the agent prefixes every command with
cd <long-path> && …, which turns every tool pill in the transcript into
identical truncated noise; with it, commands are relative and readable, and
the worker inherits project-scoped steering files.
Positioning — your app is NOT in an iframe
App UIs mount directly into the dashboard DOM. position: fixed therefore
escapes your panel and covers the ENTIRE dashboard (sidebar, header). For
overlays/modals scoped to your app: set position: relative on your app root
and use position: absolute; inset: 0 for the overlay.
Backend Change Ergonomics
UI hot-swaps in ~1s (dev mode); backend hooks load only on gateway restart or
an app disable→enable cycle. Batch backend edits and plan one reload.
Validation loop: python3 -m py_compile backend/routes.py then copy to the
installed dir — it takes effect on the NEXT reload, silently. Track what's
pending.
Verify the served UI module actually updated before debugging "my change
doesn't work": curl -s <gateway>/apps/<name>/ui/index.mjs | md5sum vs
md5sum <src>/ui/index.mjs. A mismatch means clobbered symlink or dev mode
off.
Probe a backend route without auth plumbing: an auth-gated route returning
401/403 proves it is REGISTERED; 404 means the module didn't load.
Graduating an External App to a Builtin
When an app proves out and should ship with KiroCrew, port it into the repo —
the contracts CHANGE on both sides. Template: src/kiro_crew/apps/builtins/issue_radar/.
Layout: src/kiro_crew/apps/builtins/<snake_name>/ with app.json,
backend/routes.py, optional skills/<skill>/SKILL.md, tests/.
Backend contract flips: builtins use register_routes(app: web.Application) -> None
registering FULL paths (/api/apps/<name>/…) directly on the router — the
external AppRoute-list/RouteRegistry contract does not apply. Wrap every
handler in an enabled-check gate (see issue_radar's _require_enabled):
builtin routes exist at startup even while the app is disabled.
Wiring: in-process builtin backends must be listed in
BUILTIN_NAMES (apps/builtins/__init__.py) — that startup loop is what
calls register_routes. (Subprocess-backend builtins like dev_fleet use
backend.entryPoint + port instead and are NOT listed.) App Store discovery
is separate and automatic via discover_builtin_apps() scanning app.json.
UI becomes a real React page: website/src/apps/<name>/…Page.tsx
registered in website/src/apps/builtinRegistry.ts (lazy import). You now
import MarkdownRenderer, lucide-react, the ui kit, and app-sdk components
directly — delete the module-map feature detection and CSS override hacks.
Note ChatEmbed requires an AppApiProvider ancestor; builtin pages mount
their own.
Icon/assets: website/public/app-assets/<name>/; manifest iconUrl/app-assets/<name>/icon.svg. Skills ride manifest.skills (paths relative
to the app root), registered at enable-time.
Keep defaultEnabled: false; users opt in via the App Store.
Full worktree + build-gate discipline applies (see the kirocrew-worktree-dev
skill) — this is now KiroCrew source.