with one click
coder-modules
// Creates and updates Coder Registry modules with proper scaffolding, Terraform testing, README frontmatter, and version management
// Creates and updates Coder Registry modules with proper scaffolding, Terraform testing, README frontmatter, and version management
| name | coder-modules |
| description | Creates and updates Coder Registry modules with proper scaffolding, Terraform testing, README frontmatter, and version management |
Coder Registry modules are reusable Terraform components that live under registry/<namespace>/modules/<name>/ and are consumed by templates via module blocks.
Before writing or modifying any code:
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.main.tf to understand patterns, variable conventions, and how they solve similar problems. Avoid duplicating existing functionality.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.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.
https://coder.com/docs/@v{MAJOR}.{MINOR}.{PATCH} (e.g. https://coder.com/docs/@v2.31.5)latest with 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 |
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 templateIf 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 minimumavatar field pointing to ./.images/avatar.svgThe 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).
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.snake_case (no hyphens; validation rejects them)agent_id (string, required, no default)order (number, default null, controls UI position)locals {} for computed values: URL normalization, base64 encoding, file() script content, config assemblymodule 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.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).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").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:
display_name, directly below frontmatterregistry.coder.com/<ns>/<module>/coder and pinned versiontf (NOT hcl)../../../../.icons/)> [!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
}
Modules reference icons in two places with different path systems:
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:
.icons/ directory at the repo root for available SVGs. For /icon/ paths, look at what similar modules already use..icons/ and /icon/, use those.../../../../.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.coder.svg or terminal.svg.Modules use three patterns for shell logic, depending on complexity:
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).
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).
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_data to mock data sources like coder_workspace and coder_workspace_ownercommand = apply when testing outputs or computed valuesexpect_failures to test validation rulesregexall() / startswith() / endswith() for string assertionscoder_env, coder_script, coder_app resource attributesrun "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,
]
}
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)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).
| 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 |
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.
Before considering the work complete, verify:
bun run tftest and bun run tstestbun run fmt has been runbun run shellcheck passes if the module includes shell scripts|| echo "Warning..." for non-fatal failures)../../../../.icons/), not absolute. External hyperlinks to docs or other websites are fine.In your response, include:
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..icons/ directory and the coder/coder repo at site/static/icon/.