# Coder Modules

> Creates and updates Coder Registry modules with proper scaffolding, Terraform testing, README frontmatter, and version management

- Skill: `coder/coder-modules` (Agent Skill)
- Install (CLI): `npx skillmds@latest add coder/coder-modules`
- Raw SKILL.md: https://api.skillmd.com/api/skills/coder/coder-modules/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: coder (https://skillmd.com/u/coder)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/coder/coder-modules

---


# Coder Modules

Coder Registry modules are reusable Terraform components that live under `registry/<namespace>/modules/<name>/` and are consumed by templates via `module` blocks.

## Before You Start

Before writing or modifying any code:

1. **Understand the request.** What tool, integration, or functionality is the module providing? What Coder resources does it need (`coder_script`, `coder_app`, `coder_env`, etc.)? Read the official documentation for the target tool or integration (installation steps, CLI flags, config files, environment variables, ports) so you can implement the module properly without guessing.
2. **Research existing modules.** Search the registry for similar modules. Read their `main.tf` to understand patterns, variable conventions, and how they solve similar problems. Avoid duplicating existing functionality.
3. **Check the Coder provider docs.** Verify that the resources and attributes you plan to use exist in the provider version you're targeting. Use the version-specific docs URL if needed.
4. **Clarify before building.** If the request is ambiguous (e.g. unclear which Coder resource to use, whether a `coder_app` vs `coder_script` is appropriate, what variables to expose, or which namespace to use), ask for clarification rather than guessing. Never assume a namespace; always confirm with the user.
5. **Plan the structure.** Decide on script organization (root `run.sh`, `scripts/` directory, or inline), what variables to expose, and what tests to write.

Always prefer the proper implementation over a simpler shortcut. Modules are infrastructure that users depend on. Doing less work is not the same as reducing complexity if it leaves the module incomplete or fragile.

## Documentation References

### Coder

- Coder docs (latest): <https://coder.com/docs>
- Version-specific Coder docs: `https://coder.com/docs/@v{MAJOR}.{MINOR}.{PATCH}` (e.g. <https://coder.com/docs/@v2.31.5>)
- Coder Registry: <https://registry.coder.com>

### Coder Terraform provider

- Provider docs (latest): <https://registry.terraform.io/providers/coder/coder/latest/docs>
- Version-specific provider docs: replace `latest` with a version number (e.g. <https://registry.terraform.io/providers/coder/coder/2.13.1/docs>)

Resources:

| Resource         | Docs                                                                                 |
| ---------------- | ------------------------------------------------------------------------------------ |
| `coder_app`      | <https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app>      |
| `coder_script`   | <https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script>   |
| `coder_env`      | <https://registry.terraform.io/providers/coder/coder/latest/docs/resources/env>      |
| `coder_metadata` | <https://registry.terraform.io/providers/coder/coder/latest/docs/resources/metadata> |

Data sources:

| Data Source             | Docs                                                                                           |
| ----------------------- | ---------------------------------------------------------------------------------------------- |
| `coder_parameter`       | <https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/parameter>       |
| `coder_workspace`       | <https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace>       |
| `coder_workspace_owner` | <https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace_owner> |

## Scaffolding a New Module

Only use this when creating a brand new module that does not yet exist. When updating an existing module, edit its files directly.

From repo root:

```bash
./scripts/new_module.sh namespace/module-name
```

Names must be lowercase alphanumeric with hyphens (e.g. `coder/my-tool`). Underscores are not allowed.

Creates `registry/<namespace>/modules/<module-name>/` with:

- `main.tf`: Terraform config with common resource patterns and variables — read this as the primary reference for module structure
- `README.md`: frontmatter and usage examples
- `MODULE_NAME.tftest.hcl`: Terraform native tests
- `run.sh`: install/start-up script template

If the namespace is new, the script also creates `registry/<namespace>/` with a README. New namespaces additionally need:

- `registry/<namespace>/.images/avatar.svg` (or `.png`): square image, 400x400px minimum
- The namespace README `avatar` field pointing to `./.images/avatar.svg`

The scaffolding script does not create the `.images/` directory or avatar file. When a new namespace is created, create `registry/<namespace>/.images/` and add a placeholder `avatar.svg` so the directory structure is ready for the user to replace with their real avatar.

The generated namespace README contains placeholder fields (`display_name`, `bio`, `status`, `github`, `avatar`, etc.) that the user must fill out. The `status` field is required and must be `official`, `partner`, or `community` (typically `community` for new contributors).

## Key Patterns

- Provider version constraints must reflect actual functionality requirements. Only raise the minimum `coder` provider version (e.g. `>= 2.5` to `>= 2.8`) when the module uses a resource, attribute, or behavior introduced in that version; check the provider changelog to confirm.
- Variable names MUST be `snake_case` (no hyphens; validation rejects them)
- New variables must have sensible defaults for backward compatibility
- Common variable: `agent_id` (string, required, no default)
- Common variable: `order` (number, default `null`, controls UI position)
- Use `locals {}` for computed values: URL normalization, base64 encoding, `file()` script content, config assembly
- Modules can consume other registry modules via `module` blocks (e.g. `cursor` uses `vscode-desktop-core`, CLI wrappers use `agentapi`). Before consuming a module, read its `main.tf` and `README.md` to understand the full interface: accepted variables, outputs, prerequisites, and runtime requirements. If you are inside the registry repo, read these files directly. Otherwise, read the module's page at `https://registry.coder.com/modules/<namespace>/<module-name>` which includes the full source, README, and variable definitions. Never pass arguments without confirming they exist.
- Most modules expose configuration via `variable` blocks, letting the template pass values. Use `coder_parameter` inside a module only when the module needs to present a UI choice directly to the workspace user (e.g. region selectors, IDE pickers).
- For parameter-only modules (region selectors, etc.), use `dynamic "option"` with `for_each` from a `locals` map and expose an `output` for the selected value.
- `coder_script` icons use the `/icon/<name>.svg` format. The `display_name` is typically the product name (e.g. "code-server", "Git Clone", "File Browser").
- Do not add comments that narrate what the code does or label sections. Only comment when explaining something non-obvious (e.g. why a workaround exists, a subtle constraint, or an unusual design choice).

## README.md

Required YAML frontmatter:

```yaml
---
display_name: My Tool
description: Short description of what this module does
icon: ../../../../.icons/tool.svg
verified: false
tags: [helper, ide]
---
```

Content rules:

- Single H1 heading matching `display_name`, directly below frontmatter
- When increasing header levels, increment by one each time (h1 -> h2 -> h3, not h1 -> h3)
- Usage snippet with `registry.coder.com/<ns>/<module>/coder` and pinned `version`
- Code fences labeled `tf` (NOT `hcl`)
- Relative icon paths (e.g. `../../../../.icons/`)
- **Do NOT include tables or lists that enumerate variables, parameters, or outputs.** The registry generates variable and output documentation automatically from the Terraform source. Describe what the module does and how to use it in prose, not by listing every configurable field.
- Usage examples are encouraged
- Use [GFM alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) for callouts: `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, `> [!CAUTION]`

```tf
module "my_tool" {
  count    = data.coder_workspace.me.start_count
  source   = "registry.coder.com/namespace/my-tool/coder"
  version  = "1.0.0"
  agent_id = coder_agent.main.id
}
```

## Icons

Modules reference icons in two places with different path systems:

- **README frontmatter** `icon:` uses a relative path to the repo's `.icons/` directory (e.g. `../../../../.icons/my-tool.svg`). Displayed on the registry website.
- **`coder_script` / `coder_app`** `icon =` uses an absolute `/icon/<name>.svg` path served by the Coder deployment from `site/static/icon/` in the `coder/coder` repo. Displayed in the workspace agent bar.

Workflow:

1. **Check what exists.** List the `.icons/` directory at the repo root for available SVGs. For `/icon/` paths, look at what similar modules already use.
2. **Use existing icons when they fit.** If the tool already has an icon in `.icons/` and `/icon/`, use those.
3. **When an icon doesn't exist,** reference the expected path anyway (e.g. `../../../../.icons/my-tool.svg` and `/icon/my-tool.svg`) so the structure is correct. Try to source the official SVG from the tool's branding page or repository. If you can obtain it, add it to `.icons/` in this repo.
4. **Don't substitute a generic icon.** If the tool has its own brand identity, use the correct name even if the file doesn't exist yet. Don't fall back to generic icons like `coder.svg` or `terminal.svg`.
5. **Track missing icons** so you can report them in your response.

## Scripts

Modules use three patterns for shell logic, depending on complexity:

### Root `run.sh` + `templatefile()` (simple modules)

A single `run.sh` at the module root, loaded via `templatefile()` to inject Terraform variables. Used by `code-server`, `vscode-web`, `git-clone`, `dotfiles`, `filebrowser`.

```tf
resource "coder_script" "my_tool" {
  agent_id     = var.agent_id
  display_name = "My Tool"
  icon         = "/icon/my-tool.svg"
  script = templatefile("${path.module}/run.sh", {
    LOG_PATH : var.log_path,
  })
  run_on_start = true
}
```

Use `$${VAR}` (double dollar) in the shell script for Terraform `templatefile` escaping.

If a script sources external files (`$HOME/.bashrc`, `/etc/bashrc`, `/etc/os-release`), the `source` statement must come before `set -u`; CI enforces this ordering.

### `scripts/` directory + `file()` (complex modules)

Separate `scripts/install.sh` and `scripts/start.sh` loaded via `file()` into `locals`, then passed to a child module or encoded inline. Used by `coder/claude-code`, `coder-labs/copilot`, `coder-labs/codex`, `coder-labs/cursor-cli` for example.

```tf
locals {
  install_script = file("${path.module}/scripts/install.sh")
  start_script   = file("${path.module}/scripts/start.sh")
}
```

Use `file()` when scripts don't need Terraform variable interpolation. For config templates, use a `templates/` directory with `templatefile()` (e.g. `templates/agent-config.json.tpl`).

### Inline heredoc (minimal modules)

For trivial logic, embed the script directly in the `coder_script` resource. Used by `cursor`, `zed`.

Modules that use a `scripts/` directory often also have a `testdata/` directory containing mock scripts for testing (e.g. `testdata/my-tool-mock.sh`).

## Testing

### .tftest.hcl (Required)

Every module must have Terraform native tests. The file can be named `main.tftest.hcl` or `<module-name>.tftest.hcl`. Use `command = plan` for most cases:

```hcl
run "plan_with_defaults" {
  command = plan

  variables {
    agent_id = "test-agent-id"
  }

  assert {
    condition     = var.agent_id == "test-agent-id"
    error_message = "agent_id should be set"
  }
}

run "custom_port" {
  command = plan

  variables {
    agent_id = "test-agent-id"
    port     = 8080
  }

  assert {
    condition     = resource.coder_app.my_tool.url == "http://localhost:8080"
    error_message = "App URL should use configured port"
  }
}
```

Advanced patterns:

- `override_data` to mock data sources like `coder_workspace` and `coder_workspace_owner`
- `command = apply` when testing outputs or computed values
- `expect_failures` to test validation rules
- `regexall()` / `startswith()` / `endswith()` for string assertions
- Assert on `coder_env`, `coder_script`, `coder_app` resource attributes

```hcl
run "with_mocked_workspace" {
  command = apply

  variables {
    agent_id = "foo"
  }

  override_data {
    target = data.coder_workspace.me
    values = {
      name = "test-workspace"
    }
  }

  assert {
    condition     = output.url == "expected-value"
    error_message = "URL should match expected format"
  }
}

run "validation_rejects_conflict" {
  command = plan

  variables {
    agent_id       = "test"
    option_a       = true
    option_b       = true
  }

  expect_failures = [
    var.option_a,
  ]
}
```

### main.test.ts (Optional)

For more complex testing (Docker containers, script execution, HTTP mocking).
Import from `~test` (mapped to `test/test.ts` via `tsconfig.json`):

```typescript
import { describe, expect, it } from "bun:test";
import {
  runTerraformApply,
  runTerraformInit,
  testRequiredVariables,
  findResourceInstance,
} from "~test";

describe("my-tool", () => {
  it("should init successfully", async () => {
    await runTerraformInit(import.meta.dir);
  });

  testRequiredVariables(import.meta.dir, {
    agent_id: "test-agent",
  });

  it("should apply with defaults", async () => {
    const state = await runTerraformApply(import.meta.dir, {
      agent_id: "test-agent",
    });
    const app = findResourceInstance(state, "coder_app");
    expect(app.slug).toBe("my-tool");
    expect(app.display_name).toBe("My Tool");
  });
});
```

### Test utility API (`~test`)

**Terraform helpers:**

- `runTerraformInit(dir)`: runs `terraform init`.
- `runTerraformApply(dir, vars, customEnv?)`: runs `terraform apply` with a random state file and returns `TerraformState`. Variables are passed as `TF_VAR_*`. Safe to run in parallel. `TerraformState` has `outputs: Record<string, TerraformOutput>` and `resources: TerraformStateResource[]`.
- `testRequiredVariables(dir, vars)`: auto-generates test cases (one success with all vars, plus one per var verifying apply fails without it). Pass `{}` if there are no required vars.
- `findResourceInstance(state, type, name?)`: finds the first resource instance by type. Throws if not found. Optionally filters by name.

**Docker helpers** (require `--network host`, Linux/Colima/OrbStack):

- `runContainer(image, init?)`: starts a detached container and returns its ID. Labeled `modules-test=true` for auto-cleanup.
- `removeContainer(id)`: force-removes a container.
- `execContainer(id, cmd[], args?[])`: runs a command in a container and returns `{ exitCode, stdout, stderr }`.
- `executeScriptInContainer(state, image, shell?, before?)`: finds `coder_script` in state, runs it in a container, and returns `{ exitCode, stdout: string[], stderr: string[] }`.

**File helpers:**

- `writeCoder(id, script)`: writes a mock `coder` CLI to `/usr/bin/coder` in the container.
- `writeFileContainer(id, path, content, { user? })`: writes a file to the container via base64.
- `readFileContainer(id, path)`: reads a file from the container as root.

**HTTP helpers:**

- `createJSONResponse(obj, statusCode?)`: creates a `Response` with a JSON body (defaults to 200).

Cleanup of `*.tfstate` files and `modules-test` Docker containers is handled automatically by `setup.ts` (preloaded via `bunfig.toml`).

## Commands

| Task             | Command                                               | Scope      |
| ---------------- | ----------------------------------------------------- | ---------- |
| Format all       | `bun run fmt`                                         | Repo       |
| Terraform tests  | `bun run check:terraform`                             | Repo       |
| TypeScript tests | `bun run check:typescript`                            | Repo       |
| Single TF test   | `terraform init -upgrade && terraform test -verbose`  | Module dir |
| Single TS test   | `bun test main.test.ts`                               | Module dir |
| Validate         | `./scripts/terraform_validate.sh`                     | Repo       |
| ShellCheck       | `bun run check:shellcheck`                            | Repo       |
| Version bump     | `.github/scripts/version-bump.sh patch\|minor\|major` | Repo       |

## Version Management

Bump version via `.github/scripts/version-bump.sh` when modifying modules:

- `patch`: bugfixes
- `minor`: new features, new variables with defaults
- `major`: breaking changes (removed inputs, changed defaults, new required variables)

The script automatically updates `version` references in README usage examples.

## Final Checks

Before considering the work complete, verify:

- Tests pass: `bun run check:terraform` and `bun run check:typescript` (or `bun check` to run everything)
- `bun run fmt` has been run
- `bun run check:shellcheck` passes if the module includes shell scripts
- New variables have sensible defaults for backward compatibility
- Breaking changes are documented if any inputs were removed, defaults changed, or new required variables added
- Shell scripts handle errors gracefully (`|| echo "Warning..."` for non-fatal failures)
- No hardcoded values that should be configurable via variables
- Asset and icon paths in frontmatter and Terraform must be relative (e.g. `../../../../.icons/`), not absolute. External hyperlinks to docs or other websites are fine.

## Response to the User

In your response, include:

- If a new namespace was created, remind the user to fill out the namespace README (`display_name`, `bio`, `status`, `github`, etc.) and replace the placeholder avatar. Note that this is only needed if they plan to contribute to the registry.
- If any icons were referenced but not found, list them and note they need to be sourced and added to both this repo's `.icons/` directory and the `coder/coder` repo at `site/static/icon/`.
- A note that to contribute the module to the public registry, they can open a pull request to <https://github.com/coder/registry>.

