Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

← All guides

Package authoring guide

Official Kody guide

Use this guide when creating a new Kody package or materially changing an existing one.

Choose an authoring lane

There are two lanes for writing package source. Pick based on whether you have local filesystem and git access:

  • Git lane (coding agents — preferred). Call package_get_git_remote with create: true and a new kody_id to register a stub saved package and mint a short-lived authenticated remote in one call (for existing packages, omit create). Run the returned setup_commands to clone into a temporary directory, edit normally — binary assets, multi-file refactors, and local build/test loops all work — commit, push, then publish with package_publish_external_push.
  • Tool-only lane. Without local filesystem/git access, create with package_save (complete UTF-8 text file set; no binary files) and edit through repo sessions (repo_open_session, repo_edit_files, repo_write_file, repo_commit, then repo_run_checks before repo_publish_session).

If a request needs binary assets, many-file changes, or local build/test loops and you are tool-only, tell the user the task fits a coding-capable agent better and confirm before proceeding.

README Intent section

Package intent is human-authored guidance, not a Kody primitive. Keep it in the root README.md so agents see it during package creation, updates, and search detail review.

When you create or materially change a package:

  1. Include or maintain a ## Intent section in README.md.
  2. Capture the user's goal in a few concrete sentences.
  3. Ask the user when the intent is unclear or underspecified.
  4. Update the intent only when you are confident the goal changed.
  5. If the user expands the package scope, update the section with the new scope.

Do not add a package manifest field, runtime object, saved value, or other Kody primitive solely to track intent.

Minimal shape

# Package Name

## Intent

This package exists to ...

Keep the section concise. It should explain why the package exists and what success means for the user, not duplicate every implementation detail.

Package app routing

Hosted package apps are mounted at /@username/packages/<kody-id>/<path>. The app receives only /<path> in its fetch request. Root-relative links such as /audio/123 therefore escape the mount and are not routed back to the package app.

Import packageContext from kody:runtime and build every in-app link, redirect, share/email URL, and OAuth callback against its public base:

import { packageContext } from 'kody:runtime'

if (!packageContext?.hostedUrl) {
	throw new Error('This module must run as a package app.')
}

const audioUrl = new URL(
	`${packageContext.appBasePath}/audio/123`,
	packageContext.hostedUrl,
)
  • packageContext.hostedUrl is the full public URL of the app mount.
  • packageContext.appBasePath is the origin-relative mount path.

Kody derives both fields from the package's current serving username and kody.id, including after a rename or fork. Do not hard-code either path segment.

kody.description (short public tagline)

package.json#kody.description is a short public tagline, not a feature dump. Aim for about 80–120 characters (hard max 200). Prefer outcome phrasing such as “Send transactional email via Resend” over inventory lists of exports, auth, or APIs.

Put feature lists, API surface, auth notes, and longer guidance in README.md (including ## Intent), kody.searchText, and export/JSDoc docs — not in kody.description. Community listings and Open Graph share cards reuse this field, so keep it concise.

Package visibility (private)

Default new saved packages to "private": true in package.json unless the user explicitly wants public community publishing.

Like npm, "private": true blocks community listings on this deployment. Account publishing is unaffected, so the owner can run the package privately.

  • Set "private": true when creating or forking a package unless the user asks to share it publicly.
  • Require explicit user approval before changing "private" or creating a package without "private": true.
  • Pass confirm_private_visibility_change: true on package_save or repo publish only after that approval.
  • Community publish additionally requires "private": false or omitting private, plus MIT license and README ## Intent.

Secret-using packages

When a package will use user-scoped secrets ({{secret:name}} placeholders or kody.secretMounts):

  1. Ensure each secret exists (see guide: "connect_secret" / guide: "secret_backed_integration").
  2. Self-authored packages (and community forks adopted with community_fork_adopt after a real source review) get automatic read/use access to user secrets (host approval still applies; secret_set / secret_delete still need an allowed_packages grant). After save/publish, read pending_secret_package_approvals from the tool result — it is non-null only for unadopted community forks.
  3. When pending approvals are present, either review the fork source and call community_fork_adopt with a review_summary, or send the user bulk_approval_url / each approval_url.
  4. Wait for approval or adoption (when required), then smoke-test with a keyless packages.invoke(...) call. The smoke test must go through packages.invoke — it runs the export in the package's own runtime, so kody.secretMounts mounts are exercised; a static import cannot verify those. Use a read-only export or a package-supported dry-run input that actually reads the approved secret (for example an authenticated read-only API call), so the smoke test proves secret access without external side effects.
  5. Only then treat the package as ready to run.

Host approval (from an earlier ad hoc execute smoke test) is separate from package approval. Unadopted community-forked packages may need both; self-authored and adopted packages still need host approval when outbound calls require it.

Verify your publish

After publish succeeds — and after any required secret approvals — run synthetic smoke tests for every declared surface before calling the package complete. Synthetic invocations are real-surface runs with real side effects; use a deliberately visible irreversible-side-effect guard when a smoke test should stay safe.

  1. Read test_hints on the package_publish_external_push result when present. It lists copy-pasteable calls for declared apps and subscription topics.
  2. Exports and secret mounts — keyless packages.invoke({ kodyId, exportName, params }) against a read-only export or package-supported dry-run input that exercises approved secrets (see Secret-using packages above).
  3. Package appspackage_app_fetch({ kody_id }) with the path, method, and body your handler needs. Confirm { status, headers, body, truncated } and any packageStorage() side effects. See Package app fetch.
  4. Subscriptionspackage_subscription_dispatch({ kody_id, topic, … }) with exactly one of params (fixture) or email_message_id (stored-mail replay) for each declared topic. See Synthetic event dispatch and the package subscriptions guide.
  5. Optional UI checks — open hosted_app_url when the publish response includes one; synthetic app fetches do not replace browser verification for layout, OAuth redirects, or websocket facets.

Only after these checks pass (or the user explicitly skips a surface) treat the package as ready to run.

Community icon

Public community packages should include one root community-icon.svg, community-icon.png, community-icon.webp, community-icon.jpg, or community-icon.jpeg. Prefer a square visual with a simple silhouette that remains legible at 56 pixels. Keep it under 2 MiB and 16 megapixels. Kody generates a package-name fallback when the repository has no icon.

Publishing the package refreshes the community listing icon automatically. The candidate paths win in the order listed above, so when replacing an icon with a different format (for example svg → png), delete the old file in the same commit or the old format keeps being served.

Working with an agent? This guide is also plain markdown at /guides/package-authoring.md, or load it over MCP with coding_guide_get({ guide: 'package_authoring' }).