Contributing

June 1, 2026 ยท View on GitHub

Thanks for your interest in contributing! This guide covers local development, testing, and the release process.

Setting up

Prerequisites

  • Node.js 20 or newer (CI runs against 20 and 22).
  • A local Fence build on your PATH. The plugin spawns fence --opencode-pre-tool-use on each bash tool call, so an out-of-date or missing fence binary will surface as a runner error.

First-time setup

git clone https://github.com/fencesandbox/opencode-fence.git
cd opencode-fence
npm install

Workflow

npm run lint         # biome check src
npm run typecheck    # tsc --noEmit (catches editor-only type errors)
npm test             # vitest run
npm run build        # tsc -p tsconfig.build.json (writes dist/)

CI (.github/workflows/ci.yml) runs all four on every push and PR. prepublishOnly runs lint + typecheck + test + build before npm sees the package.

For watch-mode iteration:

npm run test:watch                       # vitest in watch mode
npx tsc -p tsconfig.build.json --watch   # rebuild dist/ on save

Testing the plugin against a real OpenCode session

Three install modes, in order of how often you'll want them during development:

Point OpenCode at your dist directly. OpenCode skips its npm-install step for file:// paths:

// ~/.config/opencode/opencode.jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    "file:///absolute/path/to/opencode-fence/dist/index.js"
  ]
}

Rebuild dist (npm run build), restart OpenCode, you're testing the new code.

2. Local plugin file with the npm-published package

Once the package is on npm, you can write a local plugin shim that consumes the published version while still letting you pass createFencePlugin options:

// ~/.config/opencode/plugins/fence.ts
import { createFencePlugin } from "@fencesandbox/opencode-fence/factory";

export const Fence = createFencePlugin({
  fenceBinary: "/Users/me/go/bin/fence",
  // settingsPath, template, failOpenOnRunnerError, etc.
});

You'll need a package.json in ~/.config/opencode/ with @fencesandbox/opencode-fence as a dependency for OpenCode/Bun to resolve the import.

3. Tarball install

Useful for verifying the published package contents look right before actually publishing:

npm pack
# produces fencesandbox-opencode-fence-X.Y.Z.tgz

cd ~/.config/opencode
npm install /path/to/opencode-fence/fencesandbox-opencode-fence-X.Y.Z.tgz

OpenCode's plugin: ["@fencesandbox/opencode-fence"] then resolves to the tarball-installed copy.

Headless verification

Faster than driving the OpenCode TUI:

cd /path/to/some/repo/with/fence.json
opencode run --print-logs "run ls"

Watch the log for loading plugin path=... and any tool.execute.before activity. A working plugin denying ls shows up as Fence denied this command: ... in the output.

Release process

Releases are driven by scripts/release.sh. It bumps the package version, tags the commit, pushes, and creates a GitHub release. The publish workflow (.github/workflows/publish.yml) takes over from there: it runs lint + typecheck + test + build, then npm publish with the NPM_TOKEN secret.

./scripts/release.sh patch   # 0.1.0 -> 0.1.1
./scripts/release.sh minor   # 0.1.0 -> 0.2.0
./scripts/release.sh major   # 0.1.0 -> 1.0.0

The script will:

  1. Verify you're on main and the working tree is clean.
  2. Pull latest, confirm there are commits since the last tag.
  3. Compute the next version and ask for confirmation.
  4. Run npm version <type> to bump + commit + tag.
  5. Push the commit and tag to origin.
  6. Create a GitHub release with auto-generated notes.

The publish workflow then runs npm publish from a fresh checkout. Don't npm publish locally โ€” the workflow is the source of truth.

Prerequisites for releasing

  • gh CLI authenticated (gh auth status).
  • NPM_TOKEN secret set in the GitHub repo settings, with publish rights for @fencesandbox/opencode-fence.
  • You have push access to main and tag-creation rights.

If a release fails mid-flight

If release.sh fails after npm version but before pushing, the local commit and tag exist but origin hasn't seen them. To recover:

git tag -d vX.Y.Z       # remove the local tag
git reset --hard HEAD^  # undo the version-bump commit

Fix whatever broke, then re-run ./scripts/release.sh <type>.

If the publish workflow fails after the release was created on GitHub, you can re-run it from the Actions tab. The package isn't on npm yet, so npm publish is safe to retry. If you need to abandon the release entirely, delete the GitHub release and the tag (git push --delete origin vX.Y.Z); the next release.sh run will skip over the missing tag.