Skip to main content

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.21
  • jsr:@std/assert@1.0.0
  • npm:ms@^2 with a committed deno.lock beside the project's deno.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=/home

The 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 check

The 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
  - filesystem

The 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.yaml

You 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.yaml

To check without modifying files, add --check:

$ swamp extension fmt extensions/behaviors/manifest.yaml --check

You 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.yaml

The 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-run

The 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 version again 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 name to a collective swamp auth whoami lists.

    ... [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 --json

Compare 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-run

Swamp 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-completeness

Fix 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 example

For 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 range

The 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 spelling

Re-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 spelling

Errors 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 --json

In 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.yaml

You 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: 1

Before 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 public

Tip

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-usage

The 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.