Packages
September 20, 2026 · View on GitHub
Repos are Kody's durable home for versioned source; a package is a repo with
runtime surfaces activated (see Repos). Activation is explicit: a
root package.json alone does not make a plain repo a package —
repoPromoteToPackage (or creating through the package lanes) does.
A saved package is a repo-backed module rooted at package.json. Standard
package fields describe the package surface, and package.json#kody holds the
Kody-specific metadata.
Packages are not integrations or MCP servers. See Packages, integrations, and MCP servers when those three look interchangeable.
Mental model
Think in terms of:
- packages
- package exports
- package apps
- package subscriptions
- package-owned jobs
- package-owned retrievers
- package-owned webhooks
Packages are the saved-entity unit across search, execute, repo editing, and UI
hosting. The package page tabs are Repo, Files
(/@username/:name/tree/:ref — the same URL whether the package is public or
private), and Settings. Visibility keeps private source off the public web.
Owner controls (lock, visibility, share, webhooks, delete) live at
/@username/:name/settings. Inbound HTTP uses webhooks. To let
another paid account use a package without getting a copy,
share it.
Package state model
A saved package is a repo with the package extension activated: repos are the durable home, packages add runtime. Four concepts make up its state:
- Package source — repo-backed code and manifest rooted at
package.json(Artifacts repos plus D1entity_sourcesprojections).package.jsonis the source of truth. - Package config — configuration owned by the saved package id: manifest
metadata (
package.json#kody) and package-scoped secrets (secret buckets keyed by the saved package id, mounted viakody.secretMounts). - Package storage — the package's durable StorageRunner (SQLite) bucket,
reached via
packageStorage()fromkody:runtime. This is the durable-data primitive for every package surface: exports/invocations, subscriptions, retrievers, jobs, and package apps. Non-secret knobs and runtime state live here. - Package jobs — scheduled execution owned by the package
(
package.json#kody.jobs): schedule/execution metadata lives in D1 job rows; each job run binds a job-scoped scratch bucket; package config stays keyed by the saved package id; shared durable data goes throughpackageStorage().
When to use which:
- Durable package data →
packageStorage() - Credentials → package secrets; non-secret knobs →
packageStorage() - Scheduled work → a package job
- Per-app realtime internals → app facets (implementation detail, not the persistence mechanism)
package.json
Use package.json as the source of truth.
Important fields:
name— npm-valid scoped package name (@scope/<leaf>). This is package identity. Look up a package by that scoped name (or the name leaf). Use the saved-package UUIDpackage_idonly when the name is not known, or for a stable ref. Never pass both. The leaf after/is the URL slug. Create flows pass the@owner/leafname or the name leaf.private— leftover npm-style field; ignored for catalog listing. Visibility is a repo setting (packageUpdatechanges.visibility), default private.exports— authoritative import/export mapkody.description— short public tagline for search, detail, community listings, and share cards (~80–120 characters ideal; max 200). Prefer outcome phrasing (“Send transactional email via Resend”) over feature lists; put API surface, auth notes, and longer detail in README / Intent /searchText/ export docskody.tags— package tagskody.category— optional community browse category (integrations,examples,productivity,apps, orutilities). Community publish stores this on the listing; when it is omitted, Kody infers a category from well-known tags such asgithuborzero-auth, or files the listing under Otherkody.searchText— optional longer search text beyond the short descriptionkody.dependencies— map of direct saved package names imported through statickody:@...imports ({ "@scope/package": "*" })kody.secretMounts— optional package-scoped secret mount declarationskody.secretProvider— optional{ id }declaring this package can serve that external secret provider. Metadata alone does not bind the provider; see Custom secret providerskody.app— optional hosted package app configkody.subscriptions— optional event-topic subscriptions with package-local handlerskody.emits— optional package-emitted event topic declarationskody.webhooks— optional inbound webhook declarations bound to package exports (mint a handle withwebhookUrlMintand register it withwebhookUrlApply; see Inbound webhooks)kody.jobs— optional package-owned scheduleskody.retrievers— optional package-owned search/context retrievers
package.json is the manifest.
For predictable package resolution, saved packages must use a scoped
package.json.name. The leaf segment is the URL slug. The scope is the account
username. Changing your username on /account rewrites every saved package to
the new @{username}/… name (including same-account kody:@ imports and
kody.dependencies), publishes an automatic update commit per package, and
republishes any community listing that was already pinned to that package's
latest commit. Third-party integrations and dynamic invocations that hard-code a
previous @{username} scope need updates from their owners.
npm dependencies
Saved packages may declare runtime npm dependencies in package.json
dependencies and import them directly when they are compatible with the
Cloudflare Workers runtime.
- Kody bundles those dependencies for package exports, package apps, package-owned jobs, and package subscription handlers.
- Dependency resolution happens during package checks and publish-time artifact rebuilds, not by ad hoc package installs during normal execution.
- If a declared dependency cannot be resolved or bundled, package checks fail with the bundling error instead of allowing a publish that only fails later at runtime.
- If bundle validation exceeds the isolated check runner's memory or CPU limits, the usual cause is the npm graph, not the package source. Keep the package as a thin orchestrator and offload that work; see Offload work that does not fit a Worker isolate.
- After changing
dependencies, republish the package so Kody can rebuild the published runtime bundle artifacts that execution paths use.
Do not rely on devDependencies for saved package runtime code. Only
dependencies are treated as part of the runtime package surface.
See Cloudflare's
Node.js compatibility
for runtime details. Useful starting points include p-retry, mailparser,
remark / mdast-util-to-markdown, and googleapis; this list is not
exhaustive.
Package exports
package.json.exports is the package's callable and importable surface.
- Cross-package imports use the full package name such as
kody:@scope/my-package/export-name. Static import is the default for package reuse — from execute and from other packages — whenever the target package's name is known when the code is written. Static imports are typed, publish-verified, dependency-graph-visible, and add zero per-call platform cost. - Platform (built-in) scopes are fork-only. When a scope's username belongs
to a platform account (for example
@kody), person accounts must not statically import that package from ad hocexecuteor from a saved person-owned package. Official@kodypackages may still compose with each other. Publish checks rejectkody:@kody/…static imports andkody.dependenciesentries in person-owned package source. Execute fails the same way. Fork the official package into your scope (communityFork) and import that copy. Dynamicimport("kody:@kody/…")is unsupported. Platform packages appear insearchresults (marked with their platform scope) so agents can discover them and fork. - Static
kody:@...imports in saved package code are bundled into published runtime artifacts as snapshots of the imported package's published bundle. Republishing the imported package does not change already-published dependents; they keep using the bundled snapshot until they are republished. Ad hoc execute code bundles per call, so static imports from execute always see the current published version. - Prefer a static import when the name is known at write time. Use
import(specifier)only when the package name is data. See Package reuse. - Every direct static
kody:@...import must be declared inpackage.json#kody.dependenciesusing the imported package name, for example"dependencies": { "@scope/my-package": "*" }inside thekodyobject.*means the dependency's latest published commit, captured when this package publishes. Package checks fail when static imports and declarations differ. Type-only imports do not count, and declaration files such as.d.tsare treated as type-only. - When the target package is not known until runtime, use
import(packageSpecifier)for a caller-owned or forked module. kody:runtimeis always host-owned and request-scoped. Static imports such asimport { kody } from "kody:runtime"stay valid, but saved package artifacts do not persist Kody's runtime implementation; execution always uses the deployed host runtime.- Exports are normal modules. They may expose a default export, named exports, or both.
- Direct package invocation calls the resolved module's default export when that
export is a function. Importing a package from
executeor another package can use any named exports that the module provides. - Packages may also export non-callable helper modules and values for reuse.
- Add JSDoc to every public export. Search Purpose comes from that JSDoc;
missing comments fall back to
Package export.See Export JSDoc in the package authoring guide for the done criteria (@param,@returns, and an@examplethat importskody:@scope/id/exportand calls it). When helpful, point the export at atypesfile and put the JSDoc there. Package search detail andpackageGetsurface package descriptions, export descriptions, function signatures, JSDoc, type definitions, and FYI metadata for associated package-scoped secrets (names andpackage_id, never values). Searchpackage:{id}#{subpath}is the agent-facing door for one export contract.packageGetreturns the full export array and secret FYI.packageGetdoes not returnREADME.md,AGENTS.md, or source files. For those, open a repo session (repoOpenSession+repoReadFile) or clone withpackageGetGitRemote.
Package reuse
Package reuse follows two rules:
- Name known when the code is written → static import. Use
import fn from 'kody:@scope/my-package/export-name'from execute and from other packages. This is the default. - Name is data →
import(specifier)of a caller-owned (or forked) module. Exactly-once work uses workflows, not a second invoke primitive.
import handleEvent from 'kody:@kentcdodds/event-subscriber/handle-event'
import profile from 'kody:@kentcdodds/google/profile'
const result = await handleEvent({ event })
const account = await profile({})
Declare every static kody:@ import in package.json#kody.dependencies.
Person-owned packages must not import a platform scope; communityFork first.
packageStorage() on a static import reaches the declaring package's bucket for
caller-owned packages.
There is no author-facing packages.invoke. Interactive MCP
packageSubscriptionDispatch is the post-publish subscription smoke test, not a
composition primitive. External trusted clients that must call a named export
over HTTP use inbound webhooks: declare one webhook per export, mint a handle
with webhookUrlMint, register it with webhookUrlApply when a provider needs
the URL, and POST JSON (inputMode: "params" and Idempotency-Key for
first-party clients). See Inbound webhooks.
Scoped resolution is exact: kody:@kentcdodds/google selects a package under
that person scope that the caller owns or has an accepted
share grant to use. A platform specifier such as
kody:@kody/google is not runnable in a person account — communityFork it
first.
Package storage
Every saved package owns one durable storage bucket per user
(storageId = package:{encodeURIComponent(packageId)}), reached via
packageStorage() from kody:runtime. One rule per context:
- Writing a saved package? Use
packageStorage()for the package's own data — from package apps, exports, subscriptions, jobs, and workflows. Retrievers can only read granted buckets. - Writing ad hoc
executecode? Persist durable state from a saved package withpackageStorage(), or statically import the owning package's export. Ad hoc execute has no scratch SQLite helper. - Touching another package's data? Statically import that package's export
(
import fn from 'kody:@scope/package/export') so its stampedpackageStorage()does the reading and writing.
packageStorage() returns get/set/list/sql/delete/clear/id,
writable except in retriever runs, always bound to the declaring package's own
bucket no matter where the code runs:
- In the package's own export/invocation runtime it is the only way to reach the package bucket.
- In package apps and jobs it reaches the same shared package bucket. Keep run-scoped state in that bucket under run-scoped keys.
- When the module is statically imported (
kody:@scope/package/export) into an ad hocexecutecall or into another package, each module reads and writes the bucket of the package it came from. For caller-owned packages that is the calling user's account. For an accepted share grant,packageStorage()uses the owner's bucket so guests share one package state. Grants are per-bundle, not per-module: statically importing a package grants the whole bundle read/write access to that package's bucket, so treat static imports of unadopted community forks as a trust decision (adopt after review).
import { packageStorage } from 'kody:runtime'
export default async function listItems() {
const bucket = packageStorage()
const result = await bucket.sql('select name from items order by name asc')
return result.rows.map((row) => row.name)
}
packageStorage() identity comes from the bundler, not from source code: the
publish pipeline stamps each module with the saved package it originated from,
and execution grants bucket access only from that recorded provenance — the
running package itself and the packages the bundle statically imported.
Hand-written code cannot claim another package's id to read its bucket. Two
consequences:
- Inline
executecode has no package provenance, sopackageStorage()throws an actionable error there. Statically import the owning package's export. - Provenance grants cover directly imported packages. For data owned by a package that is not the running package and not statically imported by the bundle, import that package's export and let its stamp do the reading.
User secrets follow the same stamp. An export that reads a secret locked to its
declaring package (or declared in that package's kody.secretMounts) keeps that
authority when another package or ad hoc execute statically imports it. The
importer does not need its own allowed_packages grant. The importer's own code
still cannot read that secret, including by passing the declaring package's id
to kody.packageSecretGet / Has. See
Package approval.
Ambient storage is not a kody:runtime export
Repo checks fail when package source imports ambient storage from
kody:runtime (type-only imports and .d.ts files are exempt). Use
packageStorage() instead. Ad hoc execute has no scratch SQLite helper.
Package apps
A package app is optional.
When package.json#kody.app is present, the package is hosted under the package
app route.
Production-hosted package apps run on per-user subdomains of Kody's separate
kody.run domain (https://{username}.kody.run/packages/<package-name>/...),
not on the signed-in app origin. Opening an app from Kody performs a short-lived
session handoff to that subdomain. Package author JavaScript cannot use the
first-party kody_session cookie or call authenticated Kody pages as the
signed-in user.
Package app URLs follow the mount contract: on a subdomain the public path is
/packages/<package-name>/<path> (the username lives in the hostname; the
segment is the package name leaf). Kody strips that mount before forwarding, so
every entry sees /<path> and root-relative links such as /audio/123 escape
the app; build in-app links, redirects, shared links, email links, and OAuth
callbacks against packageContext.hostedUrl and packageContext.appBasePath
(derived from the serving username and package name leaf —
/packages/<package-name> on a subdomain, /@username/packages/<package-name>
when served inline in non-production). A Remix recipe that prefixes its route
contract remounts the Request in the entry. See
Package app routing for
the authoring example, and Package apps
(guide:package_apps) for the fetch contract, Remix recipe, session handoff,
packageAppFetch, asset URLs, and lean forks. Other saved-package runtime
surfaces may omit these app-specific fields.
Use the package app model when the package needs:
- interactive UI
- browser-side forms
- hosted callback URLs
- package-owned backend behavior
When a package app depends on OAuth, saved secrets, or a third-party API, run
the integration bootstrap first: use search for the saved integration or
secret reference, open search({ entity: "guide:integration_bootstrap" }), and
complete a minimal authenticated execute smoke test before treating the app as
ready.
A package app is a hosted Worker entry:
- app code lives in the package repo
- the entry module is declared by
kody.app.entryand default-exports a fetch handler (a function,{ fetch }, or a namedfetchexport). The host strips the app mount before forwarding. There is no runtime field; publish rejectskody.app.runtime. A leftover field on a published snapshot is ignored. Remix is a recipe (guide:package_apps#remix-recipe): the platform suppliesremix/<subpath>at the origin version as an optional convenience - Kody's runtime is available from
kody:runtime(packageStorage(),packageSecrets,kody,createAuthenticatedFetch,workflows, andpackageContext). Remix controllers can alsoget(KodyRuntime) - an optional browser entry is declared by
kody.app.client(a path, or{ entry, externals }for import-map packages); Kody bundles it for the browser on publish and serves it as a fingerprinted, immutable module under<appBasePath>/_assets/, with the URL onpackageContext.clientModuleUrl - an optional static directory is declared by
kody.app.assetsand served as-is underpackageContext.assetBasePath(see Browser client and static assets) - durable package data uses
packageStorage()— the same shared package bucket as exports and jobs - internal Durable Objects or facets are app-only realtime/coordination details layered under the package namespace, not the persistence mechanism and not separate saved primitives
Attached MCP servers
Enabled MCP servers from /account/mcp-servers are available as
kody.mcp["name"] in execute and in package runtimes that build caller context:
package apps (when capabilities or nested package imports need them), package
subscription handlers, package-owned jobs, workflows, and webhook delivery.
Package subscriptions
Package subscriptions let a saved package react to built-in Kody event topics.
Define them under package.json#kody.subscriptions as a record keyed by topic:
{
"kody": {
"subscriptions": {
"email.message.received": {
"handler": "./src/on-email-message-received.ts",
"description": "Process stored inbound mail.",
"filters": {
"inbox": "support"
}
}
}
}
}
Each subscription has:
handler— required package-local module path for the event handlerdescription— optional human-readable purpose surfaced in package detail and subscription listingsfilters— optional topic-specific metadata reserved for event dispatchers
Subscription handlers run as package runtime modules with the signed-in package
user, package-owned storage via packageStorage(), package context, secrets,
and kody:runtime helpers. Published bundle artifacts are rebuilt for
subscription handlers during package checks and publish, just like exports,
jobs, and apps.
Use the built-in packageSubscriptionsList capability to discover the signed-in
user's saved package subscriptions, optionally filtered by exact topic. This is
the generic discovery step before building fan-out, debugging why an event did
or did not dispatch, or checking which packages subscribe to
email.message.received, run.error.recorded, integration.auth.failed,
integration.auth.succeeded, mcp.server.disconnected,
mcp.server.reconnected, or admin-only topics such as status.incident.opened,
fleet.package_error_rate.elevated, fleet.entitlement.crossed,
auth.denial.burst, email.delivery.burst, user.created, user.deleted,
user.email_verification.failed, user.email_verification.stalled,
user.email_outbound.paused, and email.system-message.sent. After publish,
smoke-test one declared handler from interactive MCP with
packageSubscriptionDispatch
(Synthetic event dispatch). Reuse another
package with a static kody:@ import (Package reuse).
For accepted stored inbound email, the topic is email.message.received.
Quarantined inbound email dispatches email.message.quarantined instead. Both
payloads are metadata-first: handlers receive the stored message id, recipient
and sender metadata, timestamps, processing status, and attachment metadata.
Fetch parsed bodies or attachment bytes only when needed with emailMessageGet,
emailAttachmentGet, or the email helper from kody:runtime. Operator
system-inbox mail dispatches the separate email.system-message.received topic
to packages saved by admin users when the message is accepted (quarantined
system mail is stored but not dispatched); its payload adds an admin_url link
to the message in the admin interface. Successful operator sends from reserved
system senders dispatch email.system-message.sent with the sent correspondence
(recipients, subject, and bodies) so an admin archive package can record mail
that did not go through a utility wrapper. See
Email primitives for the full payload shapes.
When a run in your Activity finishes with an error, Kody dispatches
run.error.recorded to your packages that declare that topic. The payload is
metadata-first (run id, surface, identifiers, truncated error fields, and an
activity_url deep link). Fetch logs and full detail with runGet when needed.
See Activity and the
package subscriptions guide.
When host-side OAuth token refresh fails with reconnectable caller state, Kody
dispatches integration.auth.failed to your packages that declare that topic.
Successful refreshes and successful /connect/oauth persists dispatch
integration.auth.succeeded. Both payloads are metadata-first (connection name,
account label, scopes, timestamps, and for failed: reason, optional provider
error fields, and trusted reconnect_url / account_url; for succeeded:
source and a trusted account_url). Every classified attempt emits; notifier
packages store working ↔ failed in package storage if they want edge-triggered
pings. See the
package subscriptions guide.
When a saved MCP server leaves the ready state and stays down after a
lightweight hub retry — or parks in authenticating after a prior ready because
token refresh failed — Kody dispatches mcp.server.disconnected. Waiting and
search peeks dispatch that pending event; they do not wait for an account-page
snapshot. Recovery to ready dispatches mcp.server.reconnected with the same
episode id. The payload is metadata-first (server id/name/state, account_url).
See the package subscriptions guide.
Artifacts-backed plain repos, packages, and job sources also emit repo.pushed,
repo.created, and repo.deleted when Cloudflare Artifacts reports those
lifecycle events. Session workspace branch pushes (sessions/<id>) and publish
git-notes (refs/notes/commits) do not fan out. See Plain repos
and the package subscriptions guide for
payloads and the distinction between live HEAD and package publish.
Package webhooks
Inbound HTTP webhooks are declared under package.json#kody.webhooks and bound
to a package export. Declaring a webhook does not open ingress — mint a handle
with webhookUrlMint first, then webhookUrlApply to register a first-class
destination (GitHub repository hooks). Full contract, signature examples, and
payload shape: Inbound webhooks.
Package-owned jobs
Recurring schedules belong on a saved package:
- Define them under
package.json#kody.jobs - Reference package-local entry modules
- Schedule and execution metadata are package-owned config (D1 job rows keyed to the package)
- Each job run binds a job-scoped scratch bucket
(
job:package-job:{packageId}:{encodeURIComponent(jobName)}); that bucket is run-local - Package config (secrets and manifest mounts) stays keyed by the saved package id
- Shared durable data goes through
packageStorage()
Jobs are part of the package definition. Deferred one-shot work uses
workflows.create({ runAt }) from execute or package runtime — see
Workflows.
jobUpdate adjusts metadata on an existing job: schedule, timezone, enabled
state, kill-switch state, params, expires_at (UTC ISO auto-disable; null
clears), and preserved. Package-owned jobs keep name and source in the package
repo — change the job entry there and publish. jobDelete is rejected for
package-owned jobs for the same reason. jobRunNow triggers an existing package
job immediately for debugging. When expires_at is reached the platform stops
scheduling and auto-disables the job; that is separate from preserved, which
only skips retention deletion.
Save and edit packages
When creating a package or publishing a new version, include non-empty root
README.md and AGENTS.md. README.md is human-focused (what it does,
prerequisites, setup, done-when) and includes a concise ## Intent section.
AGENTS.md is agent-focused (imports, smoke tests, edge cases). Publish checks
fail if either file is missing or empty. Already-published packages keep running
as published; the next author-driven publish requires both files. This is a
file-layout contract, not a manifest field. Ask the user if intent is unclear,
and update Intent only when you are confident the goal changed or the user
expands the scope.
Use:
packageGetGitRemoteandpackagePublishExternalPushwhen you have a normal git client: mint a remote (passcreate: truewith the new@scope/leafname, or the name leaf, to register a stub package first), clone, edit, push, and then ask Kody to reconcile the pushed Artifacts HEAD. Existing packages use the scoped name, orpackage_idwhen the name is not known.packageSaveto create or replace a saved package from a complete UTF-8 text file set when no local git client is availablesearch({ entity: "package:{id}#{subpath}" })for one export's import specifier, types, and execute snippet;packageGetandpackageListfor saved package metadata (full export array, secret FYI). They do not return files. ForREADME.md,AGENTS.md, and source, open a repo session (repoOpenSession+repoReadFile) or clone withpackageGetGitRemotepackageDeleteto permanently remove a saved package the owner typed the name of (confirm_namemust match the package name)packageUpdateto change mutable package settings such as hidden search discovery state or to lock publishes (changes.locked: true). Unlocking is website-only.repoOpenSession,repoReadFile,repoEditFiles,repoApplyPatch,repoCommit,repoRunChecks, andrepoPublishSessionto inspect, edit, validate, and publish repo-backed package source through the file-level session API
Platform maintenance migrations (codemods)
When the platform's package API changes, Kody may migrate your published package
source with a package codemod: a versioned, deterministic, code-reviewed
transform that ships in the open-source repository. An applied codemod runs the
normal checks, records a codemod(<id>): ... commit in your package's git
history, keeps a revert snapshot, and dispatches a package.codemod.applied
event your packages can subscribe to. Unlocked packages also advance
published_commit. Locked packages still receive the commit on HEAD so you can
review and promote it later; they do not skip the transform. Ambiguous matches
are never rewritten — they surface as findings for you instead. What this does
and does not expose to deployment admins is covered in
Privacy → Platform maintenance.
Hidden packages
Use packageUpdate with the scoped name (or package_id when the name
is not known) and changes: { hidden: true } to hide a package from
ordinary ranked search. Set hidden: false inside changes to show it
again. The result includes the persisted package summary so callers can verify
the new state.
packageUpdate only accepts mutable settings. Canonical metadata including
name, description, tags, app presence, and source projection remains derived
from package.json and changes through save or publish.
Hiding is a discovery preference, not deletion. The package stays saved,
executable, and editable. Hiding is separate from visibility
(packageUpdate changes.visibility, which lists or unlists the catalog) and
from entitlement or access grants.
Delete a package
Deleting a package removes it from the account. It is permanent.
Use:
- The Delete package control under
/@username/{package-name}/settings. A modal asks you to type the package name to confirm. packageDeletewith the scoped name (orpackage_idwhen the name is not known). Show the owner the package name and what will be destroyed, wait for them to type that name, then passconfirm_namematching the package name exactly (package.jsonname, for example@you/my-package). The capability refuses the delete and names the expected value whenconfirm_nameis missing or wrong.
Delete removes the package from discovery, stops its jobs, clears package storage and package-scoped secrets, drops invocation tokens, and unlists a public catalog entry if one exists. Artifact repos are cleaned up best-effort. Existing forks keep their copies.
Hiding and making a package private are not deletion. Use those when the package should stay saved.
packageList and packageGet return a hidden boolean on each
package summary. Ranked search excludes hidden packages unless the caller
passes includeHiddenPackages: true. Known-id entity lookups still
resolve hidden packages.
Publish lock
A package with a locked_at timestamp on its
/@username/{package-name}/settings page (and on packageList / packageGet)
keeps serving its current published tree. Agents and the five-minute reconcile
job cannot advance published_commit. Use packageUpdate with
changes: { locked: true } to lock a package. Agents cannot unlock. If an
agent needs the package unlocked, it should send the owner to
/@{username}/{package-name}/settings so they can click the lock icon.
packageUpdate rejects changes.locked: false and returns that URL.
When an agent pushes or saves a locked package, the commit still lands on
Artifacts HEAD. Publish tools then return locked with an
approval_url that names that commit:
/@{username}/{package-name}/approve-publish?commit=<sha>. Opening that URL
shows a file-by-file diff of the current published tree versus that commit.
Clicking Promote this commit runs the real publish (checks, bundle
artifacts, projections) for that SHA. Promoting one commit does not unlock the
package.
Unlocked packages use the same review page. When default-branch HEAD is newer
than the last publish, the package Repo tab shows a HEAD ahead of published
badge. Owners click it to open
/@{username}/{package-name}/approve-publish?commit=<sha>, where <sha> is the
resolved default-branch HEAD, and publish that SHA with Publish HEAD.
Visitors see the badge but not the link. The five-minute reconcile job still
auto-publishes unlocked packages; this page is the explicit website path.
Fleet package-codemod apply does the same HEAD write on locked packages: the
transform commits and pushes so you can review it the next time you publish. It
does not skip locked packages, and it does not move published_commit.
When a provider token is coarser than the job — Gmail has gmail.send but no
drafts-only scope — the lock is the real grant. Worked example:
Gmail drafts without send.
Publish lock does not shrink an OAuth token or an MCP connector. To keep a connected MCP server off execute and other packages, lock the server to this package instead: Lock an MCP server to a package.
Community fork provenance
packageList and packageGet return community-fork provenance on each
package summary (source_listing_id, listing_current, listing_name,
origin_commit, listing_pinned_commit, listing_published_at,
listing_ahead). Those fields are null for self-authored packages.
listing_ahead is true only when the listing pin is not an ancestor of the fork
tip. When listing_ahead is true, the owner profile, the listing page, package
search, and package:{package-name} entity detail surface a Fork outdated /
absorb next step. Full workflow:
Public packages → Forking a listing.
Author a saved package via direct git push
Saved package source is backed by a Cloudflare Artifacts git repository. You can
create and edit it with a normal git client without round-tripping each file
change through packageSave or the file-level repo session capabilities
(repoEditFiles, etc.). This lane supports binary assets, which packageSave
and repo sessions do not.
Individual files may be at most 10 MiB (10,485,760 bytes), measured as the
file's stored byte length — UTF-8 bytes for text files, raw bytes for binary
assets. Publish checks reject any file over that per-file limit (and any source
root over the aggregate publish caps) with guidance to host the file on storage
you manage — for example Cloudflare R2, Amazon S3, Dropbox, or Google Drive —
and commit a small link or pointer file instead. Kody never rewrites your files
into pointers for you. The Artifacts remote itself also fails pushes above
roughly 32 MiB of pack content with a raw HTTP 413 before Kody is involved, so
oversized files can fail at git push with an unhelpful error even before
publish checks run.
-
Mint a short-lived remote credential:
{ "package_id": "pkg_123", "scope": "write", "ttl_seconds": 1800 }Call
packageGetGitRemotewith the scoped@owner/leafname for an existing package (orpackage_idwhen the name is not known). The result includes the plain remote URL, an authenticated one-line clone URL, anAuthorization: Bearer ...extra header,git_author(the signed-in Kody account email and display name), and setup commands that usegit -c http.extraHeader=...so the token does not need to be saved in shell history or.git/config. Those commands also set localuser.email/user.namefromgit_author. Use that identity for commits; do not invent an email.To start a new package in this lane, pass
create: truewithkody_idset to the new@owner/leafname (or the name leaf) and an optionaldescription(at most 200 characters; a short public tagline):{ "create": true, "kody_id": "@you/new-package", "description": "What this package is for" }Kody registers a private stub saved package (minimal
package.json,README.mdwith an Intent placeholder,AGENTS.mdwith import/smoke-test placeholders, and a stub root export) and returns the minted remote in the same call. Replace the stub content in your first push. -
Clone and edit:
Run every
setup_commandsentry from thepackageGetGitRemoteresult, in order, before creating commits. That sequence clones the repo,cds into it, and sets localuser.email/user.namefromgit_author. Then edit, commit, and push:git add . git commit -m "fix: update package behavior" git -c http.extraHeader='Authorization: Bearer art_v1_...' push origin HEAD:<defaultBranch>Use the default branch returned by
packageGetGitRemotefor<defaultBranch>. -
Publish the pushed Artifacts HEAD:
{ "package_id": "pkg_123" }Call
packagePublishExternalPush. Kody checks the pushed tree server-side before recording the new published version, writing the published source snapshot, rebuilding package bundle artifacts, and refreshing search projections. If the pushed HEAD is already current, the tool returnsalready_published. If the package is locked, it returnslockedwith apending_commitandapproval_urlafter checks pass and leavespublished_commitunchanged. If checks fail before promotion, it returnschecks_failedwith the failed check entries and leaves the underlying storage state unchanged. Full check-time esbuild is deferred to the later published artifact rebuild (callable and importable targets stay distinct). That rebuild copies unchanged callable, importable, app, job, and subscription artifacts from the previous published commit when the target's entry, reachable source, and bundler root config (package.json, wrangler config, vendorednode_modules) match the new snapshot; a shared-module change dirties every dependent target. Stale capturedkody:@dependency commits also force a rebuild, so republishing a stale dependent still refreshes inlined snapshots. Missing prior artifacts or snapshots, first publish, force, andalready_publishedsnapshot-mismatch repairs still rebuild. A published commit always has a complete target set — reuse writes the prior modules under the new commit key rather than leaving holes. A rebuild failure after promotion also returnschecks_failedwith a bundle check — re-run the publish capability to repair artifacts. Successfulpublishedresponses, andalready_publishedresponses when the metadata is available, include a boundedstatic_dependentssummary of direct saved packages whose published bundle artifacts statically reference this package. Stale entries mean the dependent bundle captured a dependency commit that differs from the current published commit. Kody does not automatically republish those dependents; inspect and republish only the ones whose static snapshot should reference the current published commit. Those same success results includephase_timingswith optional millisecond fieldsclone_ms,checks_typecheck_ms,checks_bundle_ms,rebuild_ms,dependents_ms, andtotal_ms. Omitted keys did not run on that attempt.checks_bundle_msis the publish bundle-check and is omitted when that esbuild pass is deferred to rebuild;rebuild_msis the later artifact rebuild — they stay separate. Readphase_timingson the capability result; it is not available fromrunListor Cloudflare Log Explorer. When an interactive publish exceeds the inline budget (~35s), the tool returnsstatus: "dispatched"with aworkflow_idinstead of hanging until the host times out.dispatchedincludesphase_timings.total_msas time-to-dispatch. PollworkflowRunListwith thatworkflow_iduntil the run finishes; do not retry the same publish while the run is active.
Dynamic package invocation is different from static bundled imports. When a runtime feature invokes another package dynamically through the package execution path, it resolves the current published package at invocation time instead of embedding a source snapshot in the dependent bundle. Dynamic invocation should not require republishing a dependent package just because the called package was republished.
Choose the narrowest token scope that fits the task. Use read for inspection
or local diffing, and write only when the git client needs to push. Keep TTLs
short for autonomous agents and CI-style helpers; the tool accepts 60 seconds to
24 hours and defaults to 4 hours.
Search and discovery
Search returns packages as the saved-entity unit.
Ranked package search hits may include a concise README excerpt when the saved package has a root README so agents can learn usage, examples, and maintenance notes without separately cloning the package repository.
Exact package detail includes nested exports, nested jobs, tags, app presence, and README content when a root README exists. Search should not frame exports or jobs as separate top-level saved entities.