CREATE AND PUBLISH
This guide shows you how to create a TypeScript extension, publish it to the Swamp registry, and install it in another repo.
If you are publishing for the first time, Publish Your First Extension walks the same pipeline step by step, and About the Extension Publishing Lifecycle explains why each step gates the next.
Prerequisites
- A Swamp repository (
swamp repo init) - A Swamp account (
swamp auth login)
Write the extension behavior
Create the extension file at extensions/behaviors/disk_usage.ts:
import { z } from "npm:zod@4";
export const behavior = {
type: "@mycollective/disk-usage",
version: "2026.04.03.1",
globalArguments: z.object({
path: z.string().describe("Filesystem path to check"),
}),
resources: {
result: {
description: "Disk usage statistics",
schema: z.object({
path: z.string(),
totalBytes: z.number(),
usedBytes: z.number(),
availableBytes: z.number(),
usedPercent: z.number(),
}),
lifetime: "infinite" as const,
garbageCollection: 10,
},
},
methods: {
check: {
description: "Check disk usage at the configured path",
arguments: z.object({}),
execute: async (
_args: Record<string, unknown>,
context: Record<string, unknown>,
) => {
const globalArgs = context.globalArgs as { path: string };
const cmd = new Deno.Command("df", {
args: ["-k", globalArgs.path],
stdout: "piped",
stderr: "piped",
});
const output = await cmd.output();
const stdout = new TextDecoder().decode(output.stdout);
const lines = stdout.trim().split("\n");
const parts = lines[1].split(/\s+/);
// `df -k` reports 1024-byte blocks on both Linux and macOS.
const totalBytes = parseInt(parts[1]) * 1024;
const usedBytes = parseInt(parts[2]) * 1024;
const availableBytes = parseInt(parts[3]) * 1024;
const usedPercent = Math.round((usedBytes / totalBytes) * 100);
const writeResource = context.writeResource as (
spec: string,
name: string,
data: Record<string, unknown>,
) => Promise<{ name: string }>;
const resultHandle = await writeResource("result", "result", {
path: globalArgs.path,
totalBytes,
usedBytes,
availableBytes,
usedPercent,
});
return { dataHandles: [resultHandle] };
},
},
},
};Replace @mycollective with your collective name. Run swamp auth whoami to
check your username and collectives.
Pin your imports
The example imports npm:zod@4; Swamp provides zod, so it needs no other pin.
Import every other dependency as an npm: or jsr: package, and either pin its
exact version or commit a lock:
npm:lodash-es@4.17.21jsr:@std/assert@1.0.0npm:ms@^2with a committeddeno.lockbeside the project'sdeno.json
Push ships each one at the exact version the bundle holds. A range with no lock
still ships pinned, at whatever version the registry serves on the day you push,
and costs one dependency-trust point. A URL import such as
https://deno.land/std@0.224.0/async/delay.ts is bundled and ships, but no
registry audit covers it, so it costs both points. See
Extension Dependencies for every
supported input.
Test the behavior locally
Create a behavior definition from the new type:
$ swamp behavior create @mycollective/disk-usage home-disk --global-arg path=/homeThe output confirms the behavior was created and lists its global arguments and methods.
Run the method to confirm it executes:
$ swamp behavior run home-disk checkThe report shows succeeded and the data the method produced.
Create the manifest
Create extensions/behaviors/manifest.yaml:
manifestVersion: 1
name: "@mycollective/disk-usage"
version: "2026.04.03.1"
description: "Check disk usage statistics for a filesystem path"
repository: "https://github.com/mycollective/disk-usage"
behaviors:
- disk_usage.ts
labels:
- monitoring
- filesystemThe name must start with your collective prefix (@username/ or @org/). Run
swamp auth whoami to check which collectives you belong to. The repository
URL is optional, but swamp extension push warns when it is absent and the
quality score rewards a verified public repository. See the
Extension Manifest reference for which
Swamp releases can load the behaviors: spelling.
Bump the version
Before each publish, get the next version from the registry:
$ swamp extension version --manifest extensions/behaviors/manifest.yamlYou will see output like:
... [INF] extension·version: Current published: (none — not yet published)
... [INF] extension·version: Next version (today): "2026.10.14.1"Update manifest.yaml with the next version. If the behavior source file also
contains a version field, update it to match.
Format, lint, and validate
Format the extension source:
$ swamp extension fmt extensions/behaviors/manifest.yamlTo check without modifying files, add --check:
$ swamp extension fmt extensions/behaviors/manifest.yaml --checkYou will see output like:
... [INF] extension·fmt: All quality checks passed.fmt formats and lints under the project Deno config, the same one push
checks against, so a clean fmt --check means the push's formatting and lint
check passes too.
Score the extension against the quality rubric. This does not gate the push — the score is informational — but it surfaces missing factors while you still have the source open:
$ swamp extension quality extensions/behaviors/manifest.yamlThe output lists each factor and whether it was earned. See Improve Your Score for per-factor fixes and the Scorecard Rubric for the full list.
Validate with a dry run before publishing:
$ swamp extension push extensions/behaviors/manifest.yaml --dry-runThe dry run builds the archive, runs every check the push runs, and asks the registry the same questions read-only. It never prompts and never uploads. The summary ends with the registry checks and the requests it made. You will see output like:
... [INF] extension·push: Dry run complete for "@skunk-ape/text-entropy@2026.10.14.1"
... [INF] extension·push: Archive size: "2.1KB"
... [INF] extension·push: Content hash: "cb98d61fbc86f1ae6a61b6e5c1f81d1e592ad20390cced35741c4cf1eb469483"
... [INF] extension·push: Registry checks:
... [INF] extension·push: authentication: passed — Signed in as skunk-ape.
... [INF] extension·push: reserved collective: passed — Collective "@skunk-ape" is not reserved.
... [INF] extension·push: collective membership: passed — Collective "@skunk-ape" is one of your collectives.
... [INF] extension·push: version exists: passed — Version 2026.10.14.1 is not published for @skunk-ape/text-entropy.
... [INF] extension·push: API calls made (3):
...With a collective API token in SWAMP_API_KEY, as in CI, the authentication
line names the token instead of a user:
... [INF] extension·push: authentication: passed — Signed in with an API token for @skunk-ape (fingerprint e83f6430f3d7a0d0).If a check fails, the dry run exits non-zero with the message the push would fail with. The two you are most likely to meet:
The version is already published, on any channel: run
swamp extension versionagain and update the manifest.... [ERR] extension·push: version exists: failed — Version 2026.09.28.1 already exists for @swamp/ssh.The collective is not one of yours: change the manifest
nameto a collectiveswamp auth whoamilists.... [ERR] extension·push: collective membership: failed — Extension collective "@acme-not-mine" is not one of your collectives (@skunk-ape, @swamp). Use one of: @skunk-ape, @swamp
A dry run that cannot reach the registry also exits non-zero, with the affected
checks listed as not run. See
Publishing Commands § Registry checks
for every check and outcome.
The dry run also prints any review warnings. You will see output like:
... [WRN] extension·push: Extension review warnings:
... [WRN] extension·push: ["medium"] "Testing Completeness" — "extensions/behaviors/text_entropy.ts": "No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing."
... [WRN] extension·push: ["medium"] "Adversarial review" — "/tmp/swamp-extension-review/_skunk-ape_text-entropy-cb98d61fbc86f1ae6a61b6e5c1f81d1e592ad20390cced35741c4cf1eb469483.json": "No adversarial review recorded for the current code — perform the review and write the report here (run with --dry-run --json for the fill-in skeleton, or see the swamp skill)."
... [WRN] extension·push: ["medium"] "Compatibility" — "(extension)": "Extension uses the behavior spelling (the behaviors: manifest key, the extensions/behaviors/ directory, export const behavior in 1 entry point). swamp clients and workers older than this release cannot load it: they read only export const model, extensions/models/ and the models: manifest key."Match CI's content hash
The content hash depends only on the extension's own files, its version, and
their layout under the extensions root, not on which Swamp repository runs the
dry run. CI publishes from a Swamp repository initialised inside the extension
directory, so a dry run from your working repository prints the hash CI reports
whenever the extensions root is the extension directory. Pass the directory that
holds manifest.yaml:
$ swamp extension push ../text-entropy --dry-run --jsonCompare contentHash in the output with the hash CI reports:
"contentHash": "cba3eff6aec80509b798a3e5ffb9581f9a884032bc5fce08050c72a495400965",The same dry run inside the extension directory, or with
--extensions-dir ../text-entropy, prints the same hash. If the hashes still
differ only by layout, the extensions root resolved somewhere else: an extension
whose entries also resolve under your repository directory keeps that directory
as its root. Pass --extensions-dir with the extension directory, or run the
dry run inside it (swamp repo init --quiet --tool none there first if it has
no .swamp.yaml). Any byte change in a packaged file, or a version change,
changes the hash.
Push from another directory
push, quality, and fmt accept the extension's directory as well as its
manifest, so you can run them from any Swamp repository. Pass the directory that
holds manifest.yaml:
$ swamp extension push ../text-entropy --dry-runSwamp finds the extension's files from the manifest's location. You will see output like:
... [INF] extension·push: Extension: "@skunk-ape/text-entropy@2026.10.14.1"
... [INF] extension·push: Requested visibility: default (registry decides)
... [INF] extension·push: License: "MIT"
... [INF] extension·push: Description: "Measure the Shannon entropy of a piece of text"
... [INF] extension·push: Repository: "https://github.com/skunk-ape/text-entropy"
... [INF] extension·push: Behaviors (1):
... [INF] extension·push: "@skunk-ape/text-entropy" ("../text-entropy/extensions/behaviors/text_entropy.ts")If an entry is not found, the error lists the paths it looked in and names the
fix: pass --extensions-dir with the directory that contains extensions/, or
add paths.base: manifest to the manifest. See
Publishing Commands § Manifest argument.
Address review warnings
Review and safety warnings do not block the push — you will be prompted to
confirm if any remain. For each one, the dry run's Unresolved warnings block
prints the fix and the exact text that would accept it:
... [INF] extension·push: Unresolved warnings:
... [INF] extension·push: testing-completeness — extensions/behaviors/disk_usage.ts: No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing.
... [INF] extension·push: fix: Add a sibling <name>_test.ts covering the success and failure paths.
... [INF] extension·push: or accept at the top of the file: // swamp-quality-ignore testing-completenessFix it. If the warning identifies a behavior file without a sibling test,
create a _test.ts file next to it that covers both success and failure paths.
Or accept it where it occurs. Paste the acceptance it printed and add a reason after a colon saying why the finding is acceptable. The acceptance is reviewed with the code in your pull request:
// swamp-quality-ignore testing-completeness: a pure function exercised by the README exampleFor a file with no comment form, such as a .txt file, or for a finding about
the whole extension, add the entry it printed to quality.yaml beside
manifest.yaml:
version: 1
accept:
- rule: ipv4-address-literals
file: hosts.txt
reason: documentation addresses from a private rangeThe behavior-spelling warning is one such extension-wide finding. Accept it
once every client and worker that installs the extension runs a release that
reads the behavior spelling — see
Extension Manifest:
version: 1
accept:
- rule: behavior-spelling
reason: every client and worker runs a release that reads the behavior spellingRe-run the dry run. Accepted findings move to Accepted warnings::
... [INF] extension·push: Accepted warnings:
... [INF] extension·push: testing-completeness — extensions/behaviors/disk_usage.ts: a pure function exercised by the README example
... [INF] extension·push: behavior-spelling — (extension): every client and worker runs a release that reads the behavior spellingErrors cannot be accepted. See Publishing Commands § Declared acceptances for every rule and form.
Write the adversarial-review report. This warning cannot be accepted. Run
the dry run with --json to get the report skeleton:
$ swamp extension push extensions/behaviors/manifest.yaml --dry-run --jsonIn the JSON output, the adversarial-review-report entry under
warnings.review has a skeleton object: a pre-filled report. Write the
skeleton to the path in the entry's file, then fill in reviewedAt and each
dimension's verdict ("pass", "issue", or "not-applicable") and note.
Re-run the dry run to confirm the warnings clear.
If you use Claude Code, the swamp-extension skill performs the review and
writes the report automatically.
Publish to the registry
$ swamp extension push extensions/behaviors/manifest.yamlYou will see output like:
... [INF] extension·push: Pushed "@mycollective/disk-usage@2026.10.14.1"
... [INF] extension·push: Channel: "stable"
... [INF] extension·push: Visibility: "public"
... [INF] extension·push: Extension ID: "bbc4f071-..."
... [INF] extension·push: Archive size: "1.4KB"
... [INF] extension·push: Behaviors: 1, Workflows: 0, Vaults: 0, Bundles: 1Before uploading, the push names the channel and asks you to confirm:
y = publish @swampadmin/text-entropy@2026.10.06.2 to stable now
N = exit; nothing is pushed
Push @swampadmin/text-entropy@2026.10.06.2 to registry (channel: stable)? [y/N]If the version already exists, the push says on which channel and offers to bump the version's micro segment. When the version is on a lower channel than the one you asked for, it also offers to promote the published build instead. See Publishing Commands § Version already published.
Add --release-notes "description of changes" to attach release notes, or add a
releaseNotes field to the manifest.
In CI, where there is no terminal, pass --yes. It answers the prompts and
waives any warning that remains; the summary lists each waived warning under
Accepted N warnings with --yes:.
To publish to a pre-release channel instead (beta or rc), see Publish Pre-release Versions.
To publish privately, add --visibility private or set visibility: private in
the manifest. The collective needs a paid plan or an active trial. Your personal
namespace qualifies when any collective you belong to has one. Otherwise the dry
run or the push stops with a message saying what the collective needs and where
to upgrade it:
Error: Collective "@ended-trial"'s 30-day free trial has ended. Private extensions need a paid plan; upgrade at https://swamp-club.com/o/ended-trial/billing.An extension's visibility is fixed by its first push; later pushes keep it. To make a private extension public once it is ready, run:
swamp extension visibility @myorg/ext --set publicTip
If you use Claude Code, the /swamp-extension-publish skill walks you through
the full publishing prerequisites — repo initialization, authentication,
manifest validation, collective ownership, version bumping, formatting, and
dry-run — before allowing the push.
Install the extension in another repo
In a different Swamp repo, pull the extension:
$ swamp extension pull @mycollective/disk-usageThe behavior type is now available. Create definitions from it with
swamp behavior create @mycollective/disk-usage <name>.
To restore pulled extensions after cloning (e.g., in CI), run
swamp extension install. See
Extension Management for the full
reference on pull, install, list, update, outdated, and rm.