---
name: buildkite-plugin-dev
description: >-
  Build, structure, and debug custom Buildkite plugins — Bash hook plugins,
  Docker/docker-compose plugins, and compiled (Go) binary plugins with a
  downloader-shim hook. Use when writing a plugin.yml, implementing agent
  lifecycle hooks (environment, checkout, command, pre-exit, etc.), mapping YAML
  plugin config to BUILDKITE_PLUGIN_* environment variables, releasing plugin
  binaries with GoReleaser, or scaffolding a new Buildkite plugin repository.
  Triggers on "Buildkite plugin", "plugin.yml", "plugin hooks", or
  "BUILDKITE_PLUGIN_".
allowed-tools: Read, Write, Edit, Grep, Glob, Bash
---

# Buildkite Plugin Development

You are an expert engineer building **production-grade custom Buildkite plugins**.
A Buildkite plugin is a small git repository the agent clones and runs at
specific points in a build via **hook scripts**. Follow these rules exactly —
they reflect how the Buildkite agent actually loads and runs plugins.

> Official docs (verify specifics here; some details evolve):
> - Writing plugins: https://buildkite.com/docs/pipelines/integrations/plugins/writing
> - Using plugins: https://buildkite.com/docs/pipelines/integrations/plugins/using
> - Plugin tools/index: https://buildkite.com/docs/pipelines/integrations/plugins
> - Agent hooks: https://buildkite.com/docs/agent/v3/hooks

## Non-negotiable rules

1. **Repo naming.** Public plugin repos are conventionally named
   `<thing>-buildkite-plugin` (e.g. `docker-buildkite-plugin`). The agent
   resolves `my-org/docker#v1.0.0` to `github.com/my-org/docker-buildkite-plugin`.
2. **Every plugin has a `plugin.yml`** at the repo root describing its name,
   author, requirements, and a JSON-Schema `configuration` block. See
   `reference/plugin-yml-schema.md`.
3. **Hooks live in `hooks/`** as executable scripts. Each hook file is named
   after the lifecycle event (`environment`, `command`, `pre-exit`, ...) with
   **no extension**, must be `chmod +x`, and must start with a shebang.
   See `reference/hooks.md` for the full list and execution order.
4. **Every hook script starts with:**
   ```bash
   #!/bin/bash
   set -euo pipefail
   ```
   (or `#!/usr/bin/env bash`). This catches unset vars and pipe failures early.
5. **Read config from environment variables, never by parsing YAML.** The agent
   converts the user's `plugins:` YAML into env vars. See
   `reference/env-var-mapping.md` for the exact naming algorithm (uppercasing,
   non-alphanumeric → `_`, array indices `_0`, `_1`, ...).
6. **Always read with a safe default:**
   `"${BUILDKITE_PLUGIN_MYPLUGIN_TIMEOUT:-10}"`. Never assume a var is set.
7. **Always quote variable expansions** (`"$VAR"`) and use `[[ ... ]]` for tests.
   Run `shellcheck` on every hook.
8. **Ship tests.** Every plugin needs Bats tests and a CI pipeline. Delegate
   the test mechanics to the `buildkite-plugin-testing` skill, but never call a
   plugin "done" without `tests/` and `.buildkite/pipeline.yml`.

## Choosing a plugin style

| Style | Use when | Template |
|-------|----------|----------|
| **Bash** | Lightweight host-side logic; export vars, wrap a command. | `templates/bash-plugin/` + `reference/bash-plugin-structure.md` |
| **Docker** | You need to isolate tooling / run the command in a container. | `templates/docker-plugin/` + `reference/building-commands.md` |
| **Compiled (Go)** | Non-trivial logic (parsing, generating pipelines, network) that deserves real tests. The hook is a thin shim that downloads a released binary. This is what the most-skilled plugins (e.g. monorepo-diff) do. | `templates/go-plugin/` + `reference/compiled-binary-plugins.md` |
| **Containerized tool** | The plugin *is* a program in a runtime you don't want on every agent (Ruby/Python/Node). The hook `docker run`s a published image; pass agent creds in so it can annotate. (e.g. test-summary) | `templates/containerized-tool-plugin/` + `reference/containerized-tool-plugins.md` |

> "Docker" above means **wrap the user's command** in a container. "Containerized
> tool" means **ship the plugin's own logic** as an image — different things.

## Workflow for a NEW plugin

1. Confirm the style from the table above (**Bash**, **Docker**, **Go**, or
   **Containerized tool**).
2. Scaffold the structure:
   ```
   <name>-buildkite-plugin/
   ├── plugin.yml
   ├── hooks/
   │   └── command            # or pre-command/post-command/environment as needed
   ├── lib/                   # optional shared shell functions, sourced by hooks
   ├── tests/
   │   └── command.bats
   ├── .buildkite/
   │   └── pipeline.yml       # runs linter + bats via plugin-tester
   ├── docker-compose.yml     # for the plugin-tester image
   ├── README.md
   └── LICENSE
   ```
3. Write `plugin.yml` first (it's the contract). Define every setting in
   `configuration.properties`, set `required`, and `additionalProperties: false`.
4. Implement the **minimum** hooks. Prefer `command` for "do the work";
   use `environment` only to export vars for later hooks; use `pre-exit` for
   cleanup that must always run.
5. Copy the matching starting point from `templates/` and adapt.
6. Add tests (see `buildkite-plugin-testing`).
7. Document every setting in `README.md` with a copy-pasteable `steps:` example.
8. **Security review before release** — run the `buildkite-plugin-security` skill
   (`scan-plugin.sh`) and resolve findings. Treat plugin config as untrusted
   input.

## Workflow for DEBUGGING an existing plugin

1. Read `plugin.yml` to learn the real env-var names (apply the mapping rules).
2. Check hook files are executable (`ls -la hooks/`) and have shebangs.
3. Look for the classic bugs: missing `set -euo pipefail`, unquoted vars,
   reading a setting under the wrong env-var name, assuming a hook runs in a
   particular directory, or doing cleanup outside `pre-exit`.
4. If you have live access, use the `buildkite-mcp-debugging` skill to pull the
   failing build's log and confirm which hook failed.

## Templates in this skill
- `templates/bash-plugin/` — a Bash hook plugin that sources reusable
  `lib/plugin.bash` helpers (`plugin_read_config`, `plugin_read_list`,
  `in_array`, ...) with unit tests for them.
- `templates/docker-plugin/` — wraps a command in a container; showcases the
  `args+=()` builder, `is_true`, UID/GID propagation, and mutual-exclusivity
  validation.
- `templates/go-plugin/` — a compiled Go plugin: downloader-shim hook,
  `main.go` + tests, `go.mod`, `.goreleaser.yml` (verified: `go test` +
  `shellcheck` pass).
- `templates/containerized-tool-plugin/` — a plugin packaged as its own Docker
  image: `Dockerfile`, `bin/run` tool, and a credential-passing wrapper hook
  with `run-without-docker` + non-fatal handling (verified: bats + shellcheck).

## References (load only when needed)
- `reference/plugin-yml-schema.md` — `plugin.yml` field-by-field.
- `reference/advanced-plugin-yml.md` — array|object settings,
  `additionalProperties`, nested step config, aliases, graceful validation.
- `reference/hooks.md` — lifecycle hooks + execution order.
- `reference/env-var-mapping.md` — YAML → env var algorithm with examples.
- `reference/bash-plugin-structure.md` — `lib/` helpers + `commands/`
  sub-command dispatch + retries/env-propagation/cross-platform, distilled from
  docker-compose-buildkite-plugin.
- `reference/building-commands.md` — the `args+=()` command builder, the
  `is_true` boolean convention, mutual-exclusivity checks, and UID/GID + env
  propagation, distilled from docker-buildkite-plugin.
- `reference/stateful-and-backend-plugins.md` — two-phase restore/save
  lifecycle, pluggable storage backends, and cache-key templating + restore-key
  fallback, distilled from cache-buildkite-plugin.
- `reference/buildkite-agent-cli.md` — using `buildkite-agent` from a hook:
  annotations (context/style, 1 MiB limit), artifact upload/download, meta-data;
  reporting-plugin etiquette (non-fatal by default). From test-summary-plugin.
- `reference/containerized-tool-plugins.md` — packaging a plugin as its own
  Docker image, and passing agent credentials in so the container can call
  `buildkite-agent`. From test-summary-plugin.
- `reference/auth-and-registry-plugins.md` — registry-login / credential
  plugins: the `environment` hook for auth, `--password-stdin`, env-var
  indirection, isolated config, `pre-exit` logout, and OIDC→ECR. From
  docker-login + ecr plugins.
- `reference/compiled-binary-plugins.md` — the Go downloader-shim + GoReleaser
  pattern, distilled from production plugins like monorepo-diff.
