From eeff982d5ba06c625bc6dd6a8035c721ee2105e2 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 23 Oct 2026 03:02:09 +0000 Subject: [PATCH 1/2] feat(coder/agent-relay-claude-code)!: make additional_args a list of strings additional_args was a map of flag to value, which cannot repeat a flag: a repeated key silently keeps only the last value. It is now a list with one element per argument, passed in order, so a flag can repeat. Flags the module sets are still rejected, with or without =value. BREAKING CHANGE: additional_args changes type from map(string) to list(string). Rewrite { "--flag" = "value" } as ["--flag", "value"]. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2TMgCPW8SL8kUCB28L4dt --- .../modules/agent-relay-claude-code/README.md | 25 ++++++----------- .../agent-relay-claude-code/main.test.ts | 28 ++++++++++++------- .../modules/agent-relay-claude-code/main.tf | 18 +++++------- .../agent-relay-claude-code/main.tftest.hcl | 25 ++++++----------- 4 files changed, 42 insertions(+), 54 deletions(-) diff --git a/registry/coder/modules/agent-relay-claude-code/README.md b/registry/coder/modules/agent-relay-claude-code/README.md index 6311750cc..d0e31f885 100644 --- a/registry/coder/modules/agent-relay-claude-code/README.md +++ b/registry/coder/modules/agent-relay-claude-code/README.md @@ -50,25 +50,18 @@ when to reap the workspace. ## Additional runner arguments -Use `additional_args` to pass extra flags to `claude self-hosted-runner`, as a -map of flag to value. They are appended after the flags the module sets. Each -flag is passed exactly as written, so use `--name` or `-n` as the CLI expects, -followed by its value as the next argument; an empty value passes the flag -alone. Values are passed verbatim, so spaces and quotes need no escaping. -Terraform orders map keys alphabetically. Flags the module already sets -(`--capacity`, `--base-dir`, `--exit-if-unused-min`, `--exec-path`) are -rejected. +Use `additional_args` to pass extra arguments to `claude self-hosted-runner`, as a +list with one element per argument. They are appended after the flags the module sets and before `--exec-path`, in the order written. +Each element is passed exactly as written, so a flag can repeat and a value +can contain spaces or quotes without escaping. Flags the module already sets +(`--capacity`, `--base-dir`, `--exit-if-unused-min`, `--exec-path`) are rejected, with or without `=value`. ```tf module "claude_code_runner" { - source = "registry.coder.com/coder/agent-relay-claude-code/coder" - version = "0.3.0" - agent_id = coder_agent.main.id - # Passed as: --another-flag --some-flag value - additional_args = { - "--another-flag" = "" - "--some-flag" = "value" - } + source = "registry.coder.com/coder/agent-relay-claude-code/coder" + version = "0.3.0" + agent_id = coder_agent.main.id + additional_args = ["--some-flag", "value", "--some-flag", "another value", "--another-flag"] } ``` diff --git a/registry/coder/modules/agent-relay-claude-code/main.test.ts b/registry/coder/modules/agent-relay-claude-code/main.test.ts index 8afec6ff5..1174a331b 100644 --- a/registry/coder/modules/agent-relay-claude-code/main.test.ts +++ b/registry/coder/modules/agent-relay-claude-code/main.test.ts @@ -307,13 +307,15 @@ describe("agent-relay-claude-code", () => { expect(env.stdout).toContain("SELF_HOSTED_RUNNER_LOCK_TO_ACCOUNT=acct-123"); }); - it("appends additional_args as one argument per flag", async () => { + it("appends additional_args in order, one argument per element", async () => { const { id, scripts } = await setup({ - additional_args: JSON.stringify({ - "--verbose": "", - "--name": "two words $HOME", - "-t": `it"s`, - }), + additional_args: JSON.stringify([ + "--name", + "two words $HOME", + "--name", + `it"s`, + "--verbose", + ]), }); await stubClaude(id, 'printf "%s\\n" "$@" >/tmp/claude-args; sleep 30'); const { start } = await runDispatched(id, scripts); @@ -321,10 +323,16 @@ describe("agent-relay-claude-code", () => { const args = (await readFileContainer(id, "/tmp/claude-args")).split("\n"); expect(args[0]).toBe("self-hosted-runner"); - expect(args).toContain("--verbose"); - // Values reach the CLI verbatim: no word splitting or expansion. - expect(args[args.indexOf("--name") + 1]).toBe("two words $HOME"); - expect(args[args.indexOf("-t") + 1]).toBe('it"s'); + // A flag can repeat, and arguments reach the CLI verbatim and in order: + // no word splitting or expansion. + const at = args.indexOf("--name"); + expect(args.slice(at, at + 5)).toEqual([ + "--name", + "two words $HOME", + "--name", + 'it"s', + "--verbose", + ]); // Module-set flags still come through, and the wrapper stays last. expect(args[args.indexOf("--capacity") + 1]).toBe("1"); expect(args[args.indexOf("--exec-path") + 1]).toBe( diff --git a/registry/coder/modules/agent-relay-claude-code/main.tf b/registry/coder/modules/agent-relay-claude-code/main.tf index 0c2a82dac..6d355d6b8 100644 --- a/registry/coder/modules/agent-relay-claude-code/main.tf +++ b/registry/coder/modules/agent-relay-claude-code/main.tf @@ -100,20 +100,20 @@ variable "serving_log_pattern" { } variable "additional_args" { - description = "Additional flags to pass to `claude self-hosted-runner`, as a map of flag to value. Each flag is passed as written (e.g. `--name` or `-n`) followed by its value as the next argument; an empty value passes the flag alone (e.g. `{ \"--some-flag\" = \"value\" }`)." - type = map(string) - default = {} + description = "Additional arguments to pass to `claude self-hosted-runner`, one list element per argument, in order (e.g. `[\"--header\", \"a: 1\", \"--header\", \"b: 2\", \"--verbose\"]`). Each element is passed as written, so a flag can repeat and a value may contain spaces." + type = list(string) + default = [] validation { - condition = alltrue([for k, v in var.additional_args : can(regex("^--?[A-Za-z0-9][A-Za-z0-9._-]*$", k)) && !can(regex("\n", v))]) - error_message = "additional_args keys must be flags such as --name or -n (letters, digits, '.', '_', '-'), and values must not contain newlines." + condition = alltrue([for a in var.additional_args : a != "" && !can(regex("\n", a))]) + error_message = "additional_args elements must be non-empty and must not contain newlines." } # The module already sets these, from base_dir, exit_if_unused_min, and the # permissions wrapper. Keep this list in sync with the `self-hosted-runner` # flags in start.sh.tftpl. validation { - condition = length(setintersection(keys(var.additional_args), ["--capacity", "--base-dir", "--exit-if-unused-min", "--exec-path"])) == 0 + condition = !anytrue([for a in var.additional_args : contains(["--capacity", "--base-dir", "--exit-if-unused-min", "--exec-path"], split("=", a)[0])]) error_message = "additional_args must not set --capacity, --base-dir, --exit-if-unused-min, or --exec-path; the module sets them (use base_dir and exit_if_unused_min instead)." } } @@ -248,10 +248,6 @@ locals { # tree so one directory holds everything a debugger needs. module_directory = "$HOME/.coder-modules/coder/agent-relay-claude-code" - # Render additional_args as argv: each key as written, followed by its - # value as a separate argument unless the value is empty. - additional_args = flatten([for k, v in var.additional_args : v == "" ? [k] : [k, v]]) - install_script = templatefile("${path.module}/install.sh.tftpl", { cli_binary = var.cli_binary install_cli = var.install_cli @@ -264,7 +260,7 @@ locals { log_file = var.log_file base_dir = var.base_dir exit_if_unused_min = var.exit_if_unused_min - additional_args = base64encode(join("\n", local.additional_args)) + additional_args = base64encode(join("\n", var.additional_args)) }) } diff --git a/registry/coder/modules/agent-relay-claude-code/main.tftest.hcl b/registry/coder/modules/agent-relay-claude-code/main.tftest.hcl index 9a58ed439..673b7c3ac 100644 --- a/registry/coder/modules/agent-relay-claude-code/main.tftest.hcl +++ b/registry/coder/modules/agent-relay-claude-code/main.tftest.hcl @@ -216,7 +216,7 @@ run "additional_args_default_empty" { command = plan assert { - condition = local.additional_args == [] && strcontains(local.start_script, "done < <(echo -n '' | base64 -d)") + condition = strcontains(local.start_script, "done < <(echo -n '' | base64 -d)") error_message = "additional_args must default to empty" } } @@ -225,30 +225,21 @@ run "additional_args_encoded" { command = plan variables { - additional_args = { - "--verbose" = "" - "--name" = "it's here" - "-t" = "x" - } + additional_args = ["--header", "it's here", "--header", "b", "--verbose"] } + # Base64 keeps quotes in an argument from breaking the rendered script. assert { - condition = local.additional_args == ["--name", "it's here", "--verbose", "-t", "x"] - error_message = "each flag must be passed as written, followed by its value unless the value is empty" - } - - # Base64 keeps quotes in a value from breaking the rendered script. - assert { - condition = strcontains(local.start_script, base64encode(join("\n", local.additional_args))) && !strcontains(local.start_script, "it's here") - error_message = "additional_args must be base64-encoded into the start script" + condition = strcontains(local.start_script, base64encode("--header\nit's here\n--header\nb\n--verbose")) && !strcontains(local.start_script, "it's here") + error_message = "additional_args must be base64-encoded into the start script, one argument per line, in order" } } -run "additional_args_rejects_bad_flag" { +run "additional_args_rejects_newline" { command = plan variables { - additional_args = { "verbose; rm -rf /" = "" } + additional_args = ["--verbose\n--capacity"] } expect_failures = [var.additional_args] @@ -258,7 +249,7 @@ run "additional_args_rejects_module_flags" { command = plan variables { - additional_args = { "--capacity" = "2" } + additional_args = ["--capacity=2"] } expect_failures = [var.additional_args] From a1f1f375bf78cc8cfa41dc58e3ff95248daef61c Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 23 Oct 2026 03:02:33 +0000 Subject: [PATCH 2/2] chore(coder/agent-relay-claude-code): bump version to 0.4.0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2TMgCPW8SL8kUCB28L4dt --- registry/coder/modules/agent-relay-claude-code/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/agent-relay-claude-code/README.md b/registry/coder/modules/agent-relay-claude-code/README.md index d0e31f885..87180cf74 100644 --- a/registry/coder/modules/agent-relay-claude-code/README.md +++ b/registry/coder/modules/agent-relay-claude-code/README.md @@ -16,7 +16,7 @@ on each build and runs the Claude Code self-hosted runner. ```tf module "claude_code_runner" { source = "registry.coder.com/coder/agent-relay-claude-code/coder" - version = "0.3.0" + version = "0.4.0" agent_id = coder_agent.main.id # Downloads the Claude Code CLI at start when it is not in the image. Bake @@ -59,7 +59,7 @@ can contain spaces or quotes without escaping. Flags the module already sets ```tf module "claude_code_runner" { source = "registry.coder.com/coder/agent-relay-claude-code/coder" - version = "0.3.0" + version = "0.4.0" agent_id = coder_agent.main.id additional_args = ["--some-flag", "value", "--some-flag", "another value", "--another-flag"] }