Skip to content

Commit 46ce616

Browse files
committed
refactor(registry/coder/modules/claude-code): make workdir optional and scope MCP to user
workdir is now optional. When set, the module still pre-creates the directory and pre-accepts the Claude Code trust dialog for it. When unset, the module installs the CLI and configures authentication only; users accept trust dialogs interactively per project. MCP servers are added at Claude Code's user scope via `claude mcp add-json --scope user` so they are available across every project the workspace owner opens, instead of being tied to a single project directory. For project-local MCP servers, callers should commit a `.mcp.json` to the project repository rather than passing it through this module. Drop primaryApiKey from the standalone-mode config writer. Claude Code reads credentials from the ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN env vars (which the module already exports via coder_env); writing the key into ~/.claude.json had no effect on authentication. Split the standalone-mode .claude.json writer into two steps: the always-on auth/onboarding keys, and the optional `.projects[workdir]` trust block that only runs when workdir is set.
1 parent c9bd6d0 commit 46ce616

5 files changed

Lines changed: 58 additions & 46 deletions

File tree

registry/coder/modules/claude-code/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@ module "claude-code" {
1515
source = "registry.coder.com/coder/claude-code/coder"
1616
version = "5.0.0"
1717
agent_id = coder_agent.main.id
18-
workdir = "/home/coder/project"
1918
anthropic_api_key = "xxxx-xxxxx-xxxx"
2019
}
2120
```
2221

22+
## workdir
23+
24+
`workdir` is optional. When set, the module pre-creates the directory if it is missing and pre-accepts the Claude Code trust/onboarding prompt for it in `~/.claude.json`. Leave `workdir` unset if you only want the module to install the CLI and configure authentication; users can still open any project interactively and accept the trust dialog per project.
25+
26+
## MCP scope
27+
28+
Servers configured through `mcp` or `mcp_config_remote_path` are added at Claude Code's [user scope](https://docs.claude.com/en/docs/claude-code/mcp#scope), which makes them available across every project the workspace owner opens. For project-local MCP servers, commit a `.mcp.json` file to the project repository instead of passing it through this module.
29+
2330
## Prerequisites
2431

2532
Provide exactly one authentication method:

registry/coder/modules/claude-code/main.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -343,10 +343,10 @@ describe("claude-code", async () => {
343343

344344
// Should contain the MCP server add command from the successful fetch.
345345
expect(installLog).toContain(
346-
"Added stdio MCP server go-language-server to local config",
346+
"Added stdio MCP server go-language-server to user config",
347347
);
348348
expect(installLog).toContain(
349-
"Added stdio MCP server typescript-language-server to local config",
349+
"Added stdio MCP server typescript-language-server to user config",
350350
);
351351

352352
// Verify the MCP config was added to .claude.json.
@@ -380,7 +380,6 @@ describe("claude-code", async () => {
380380
"/home/coder/.claude.json",
381381
);
382382
const parsed = JSON.parse(claudeConfig);
383-
expect(parsed.primaryApiKey).toBe(apiKey);
384383
expect(parsed.autoUpdaterStatus).toBe("disabled");
385384
expect(parsed.hasCompletedOnboarding).toBe(true);
386385
expect(parsed.bypassPermissionsModeAccepted).toBe(true);
@@ -405,8 +404,9 @@ describe("claude-code", async () => {
405404
expect(installLog).toContain("Standalone mode configured successfully");
406405
expect(installLog).not.toContain("skipping onboarding bypass");
407406

408-
// Onboarding bypass flags must be present; primaryApiKey is unused when
409-
// auth happens via CLAUDE_CODE_OAUTH_TOKEN.
407+
// Onboarding bypass flags must be present. Authentication happens via
408+
// the ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN env vars, not via
409+
// .claude.json.
410410
const claudeConfig = await readFileContainer(
411411
id,
412412
"/home/coder/.claude.json",

registry/coder/modules/claude-code/main.tf

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ variable "icon" {
2626

2727
variable "workdir" {
2828
type = string
29-
description = "Project directory to pre-configure for Claude Code. The module creates this directory if it is missing, registers MCP servers against it, and pre-accepts the trust/onboarding prompts for it in ~/.claude.json."
29+
description = "Optional project directory. When set, the module pre-creates it if missing and pre-accepts the Claude Code trust/onboarding prompt for it in ~/.claude.json."
30+
default = null
3031
}
3132

3233
variable "pre_install_script" {
@@ -73,13 +74,13 @@ variable "model" {
7374

7475
variable "mcp" {
7576
type = string
76-
description = "JSON-encoded string to configure MCP servers for Claude Code. When set, writes MCP configuration into the Claude Code local scope."
77+
description = "JSON-encoded string of MCP server configurations. When set, servers are added at Claude Code's user scope so they are available across every project the workspace owner opens."
7778
default = ""
7879
}
7980

8081
variable "mcp_config_remote_path" {
8182
type = list(string)
82-
description = "List of URLs that return JSON MCP server configurations (text/plain with valid JSON)"
83+
description = "List of URLs that return JSON MCP server configurations (text/plain with valid JSON). Servers are added at Claude Code's user scope."
8384
default = []
8485
}
8586

@@ -163,7 +164,7 @@ resource "coder_env" "anthropic_base_url" {
163164
}
164165

165166
locals {
166-
workdir = trimsuffix(var.workdir, "/")
167+
workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : ""
167168
install_script = templatefile("${path.module}/scripts/install.sh.tftpl", {
168169
ARG_CLAUDE_CODE_VERSION = var.claude_code_version
169170
ARG_INSTALL_CLAUDE_CODE = tostring(var.install_claude_code)

registry/coder/modules/claude-code/main.tftest.hcl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,16 @@ run "test_script_outputs_with_pre_and_post" {
270270
error_message = "scripts output should list pre_install, install, post_install in run order"
271271
}
272272
}
273+
274+
run "test_workdir_optional" {
275+
command = plan
276+
277+
variables {
278+
agent_id = "test-agent-no-workdir"
279+
}
280+
281+
assert {
282+
condition = var.workdir == null
283+
error_message = "workdir should default to null when omitted"
284+
}
285+
}

registry/coder/modules/claude-code/scripts/install.sh.tftpl

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ function add_mcp_servers() {
3838

3939
while IFS= read -r server_name && IFS= read -r server_json; do
4040
echo "------------------------"
41-
echo "Executing: claude mcp add-json \"$${server_name}\" '$${server_json}' ($${source_desc})"
42-
claude mcp add-json "$${server_name}" "$${server_json}" || echo "Warning: Failed to add MCP server '$${server_name}', continuing..."
41+
echo "Executing: claude mcp add-json --scope user \"$${server_name}\" '$${server_json}' ($${source_desc})"
42+
claude mcp add-json --scope user "$${server_name}" "$${server_json}" || echo "Warning: Failed to add MCP server '$${server_name}', continuing..."
4343
echo "------------------------"
4444
echo ""
4545
done < <(echo "$${mcp_json}" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)')
@@ -113,7 +113,7 @@ function install_claude_code_cli() {
113113
}
114114

115115
function setup_claude_configurations() {
116-
if [ ! -d "$${ARG_WORKDIR}" ]; then
116+
if [ -n "$${ARG_WORKDIR}" ] && [ ! -d "$${ARG_WORKDIR}" ]; then
117117
echo "Warning: The specified folder '$${ARG_WORKDIR}' does not exist."
118118
echo "Creating the folder..."
119119
mkdir -p "$${ARG_WORKDIR}"
@@ -124,28 +124,22 @@ function setup_claude_configurations() {
124124
mkdir -p "$${module_path}"
125125

126126
if [ "$${ARG_MCP}" != "" ]; then
127-
(
128-
cd "$${ARG_WORKDIR}"
129-
add_mcp_servers "$${ARG_MCP}" "in $${ARG_WORKDIR}"
130-
)
127+
add_mcp_servers "$${ARG_MCP}" "from module input"
131128
fi
132129

133130
if [ -n "$${ARG_MCP_CONFIG_REMOTE_PATH}" ] && [ "$${ARG_MCP_CONFIG_REMOTE_PATH}" != "[]" ]; then
134-
(
135-
cd "$${ARG_WORKDIR}"
136-
for url in $(echo "$${ARG_MCP_CONFIG_REMOTE_PATH}" | jq -r '.[]'); do
137-
echo "Fetching MCP configuration from: $${url}"
138-
mcp_json=$(curl -fsSL "$${url}") || {
139-
echo "Warning: Failed to fetch MCP configuration from '$${url}', continuing..."
140-
continue
141-
}
142-
if ! echo "$${mcp_json}" | jq -e '.mcpServers' > /dev/null 2>&1; then
143-
echo "Warning: Invalid MCP configuration from '$${url}' (missing mcpServers), continuing..."
144-
continue
145-
fi
146-
add_mcp_servers "$${mcp_json}" "from $${url}"
147-
done
148-
)
131+
for url in $(echo "$${ARG_MCP_CONFIG_REMOTE_PATH}" | jq -r '.[]'); do
132+
echo "Fetching MCP configuration from: $${url}"
133+
mcp_json=$(curl -fsSL "$${url}") || {
134+
echo "Warning: Failed to fetch MCP configuration from '$${url}', continuing..."
135+
continue
136+
}
137+
if ! echo "$${mcp_json}" | jq -e '.mcpServers' > /dev/null 2>&1; then
138+
echo "Warning: Invalid MCP configuration from '$${url}' (missing mcpServers), continuing..."
139+
continue
140+
fi
141+
add_mcp_servers "$${mcp_json}" "from $${url}"
142+
done
149143
fi
150144

151145
}
@@ -163,15 +157,11 @@ function configure_standalone_mode() {
163157
if [ -f "$${claude_config}" ]; then
164158
echo "Updating existing Claude configuration at $${claude_config}"
165159

166-
jq --arg workdir "$${ARG_WORKDIR}" --arg apikey "$${ANTHROPIC_API_KEY:-}" \
167-
'.autoUpdaterStatus = "disabled" |
160+
jq '.autoUpdaterStatus = "disabled" |
168161
.autoModeAccepted = true |
169162
.bypassPermissionsModeAccepted = true |
170163
.hasAcknowledgedCostThreshold = true |
171-
.hasCompletedOnboarding = true |
172-
.primaryApiKey = $apikey |
173-
.projects[$workdir].hasCompletedProjectOnboarding = true |
174-
.projects[$workdir].hasTrustDialogAccepted = true' \
164+
.hasCompletedOnboarding = true' \
175165
"$${claude_config}" > "$${claude_config}.tmp" && mv "$${claude_config}.tmp" "$${claude_config}"
176166
else
177167
echo "Creating new Claude configuration at $${claude_config}"
@@ -181,18 +171,19 @@ function configure_standalone_mode() {
181171
"autoModeAccepted": true,
182172
"bypassPermissionsModeAccepted": true,
183173
"hasAcknowledgedCostThreshold": true,
184-
"hasCompletedOnboarding": true,
185-
"primaryApiKey": "$${ANTHROPIC_API_KEY:-}",
186-
"projects": {
187-
"$${ARG_WORKDIR}": {
188-
"hasCompletedProjectOnboarding": true,
189-
"hasTrustDialogAccepted": true
190-
}
191-
}
174+
"hasCompletedOnboarding": true
192175
}
193176
EOF
194177
fi
195178

179+
if [ -n "$${ARG_WORKDIR}" ]; then
180+
echo "Pre-accepting trust dialog for $${ARG_WORKDIR}"
181+
jq --arg workdir "$${ARG_WORKDIR}" \
182+
'.projects[$workdir].hasCompletedProjectOnboarding = true |
183+
.projects[$workdir].hasTrustDialogAccepted = true' \
184+
"$${claude_config}" > "$${claude_config}.tmp" && mv "$${claude_config}.tmp" "$${claude_config}"
185+
fi
186+
196187
echo "Standalone mode configured successfully"
197188
}
198189

0 commit comments

Comments
 (0)