## Description Add two Claude Code skills for the Coder Registry: `coder-modules` and `coder-templates`. These skills guide AI agents through creating and updating registry modules and workspace templates, covering scaffolding, Terraform patterns, testing, README standards, icon management, version bumps, and newer features like presets, prebuilds, and task-oriented templates.
18 KiB
name, description
| name | description |
|---|---|
| coder-modules | Creates and updates Coder Registry modules with proper scaffolding, Terraform testing, README frontmatter, and version management |
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:
- 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. - Research existing modules. Search the registry for similar modules. Read their
main.tfto understand patterns, variable conventions, and how they solve similar problems. Avoid duplicating existing functionality. - 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.
- Clarify before building. If the request is ambiguous (e.g. unclear which Coder resource to use, whether a
coder_appvscoder_scriptis 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. - 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
latestwith a version number (e.g. https://registry.terraform.io/providers/coder/coder/2.13.1/docs)
Resources:
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:
./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 structureREADME.md: frontmatter and usage examplesMODULE_NAME.tftest.hcl: Terraform native testsrun.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
avatarfield 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
coderprovider version (e.g.>= 2.5to>= 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, defaultnull, controls UI position) - Use
locals {}for computed values: URL normalization, base64 encoding,file()script content, config assembly - Modules can consume other registry modules via
moduleblocks (e.g.cursorusesvscode-desktop-core, CLI wrappers useagentapi). Before consuming a module, read itsmain.tfandREADME.mdto 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 athttps://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
variableblocks, letting the template pass values. Usecoder_parameterinside 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"withfor_eachfrom alocalsmap and expose anoutputfor the selected value. coder_scripticons use the/icon/<name>.svgformat. Thedisplay_nameis 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:
---
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>/coderand pinnedversion - Code fences labeled
tf(NOThcl) - 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 for callouts:
> [!NOTE],> [!TIP],> [!IMPORTANT],> [!WARNING],> [!CAUTION]
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_appicon =uses an absolute/icon/<name>.svgpath served by the Coder deployment fromsite/static/icon/in thecoder/coderrepo. Displayed in the workspace agent bar.
Workflow:
- Check what exists. List the
.icons/directory at the repo root for available SVGs. For/icon/paths, look at what similar modules already use. - Use existing icons when they fit. If the tool already has an icon in
.icons/and/icon/, use those. - When an icon doesn't exist, reference the expected path anyway (e.g.
../../../../.icons/my-tool.svgand/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. - 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.svgorterminal.svg. - 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.
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, coder/amazon-q for example.
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. coder/amazon-q/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:
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_datato mock data sources likecoder_workspaceandcoder_workspace_ownercommand = applywhen testing outputs or computed valuesexpect_failuresto test validation rulesregexall()/startswith()/endswith()for string assertions- Assert on
coder_env,coder_script,coder_appresource attributes
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):
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): runsterraform init.runTerraformApply(dir, vars, customEnv?): runsterraform applywith a random state file and returnsTerraformState. Variables are passed asTF_VAR_*. Safe to run in parallel.TerraformStatehasoutputs: Record<string, TerraformOutput>andresources: 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. Labeledmodules-test=truefor 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?): findscoder_scriptin state, runs it in a container, and returns{ exitCode, stdout: string[], stderr: string[] }.
File helpers:
writeCoder(id, script): writes a mockcoderCLI to/usr/bin/coderin 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 aResponsewith 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 tftest |
Repo |
| TypeScript tests | bun run tstest |
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 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: bugfixesminor: new features, new variables with defaultsmajor: 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 tftestandbun run tstest bun run fmthas been runbun run shellcheckpasses 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 thecoder/coderrepo atsite/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.