Morgan Lunt 4ca251f448
feat(claude-code): add managed_settings input for policy delivery via /etc/claude-code (#863)
## Problem

The module configures Claude Code's permission posture by reaching
around the permission system rather than through it:

- `scripts/install.sh` writes `bypassPermissionsModeAccepted`,
`autoModeAccepted`, and `primaryApiKey` directly into the user-writable
`~/.claude.json`. Any process in the workspace can read the API key or
flip the acceptance flags back.
- `scripts/start.sh` adds `--dangerously-skip-permissions` to every task
launch, even when the template author set an explicit `permission_mode`.
The README has to carry a security warning telling people the module
bypasses permission checks.
- `permission_mode`, `allowed_tools`, and `disallowed_tools` each plumb
through a different ad-hoc path (CLI flag, `coder` subcommand) instead
of a single policy surface.

## Change

Add a `managed_settings` input that renders to
`/etc/claude-code/managed-settings.d/10-coder.json`. Claude Code reads
that drop-in directory at startup with the highest configuration
precedence (above `~/.claude/settings.json` and project settings), so
template authors get an admin-controlled policy file that users inside
the workspace cannot override. The mechanism is a local file read with
no API call, so it works identically for the Anthropic API, AWS Bedrock,
Google Vertex AI, and AI Bridge / AI Gateway.

```hcl
managed_settings = {
  permissions = {
    defaultMode                  = "acceptEdits"
    disableBypassPermissionsMode = "disable"
    deny                         = ["Bash(curl:*)", "WebFetch"]
  }
}
```

Supporting changes:

- `install.sh` writes the policy file (root-owned, 0644) and stops
writing `bypassPermissionsModeAccepted`, `autoModeAccepted`, and
`primaryApiKey` into `~/.claude.json`. The API key is already exported
via `coder_env` as `CLAUDE_API_KEY`; duplicating it on disk is
unnecessary. `hasCompletedOnboarding` stays because there is no env-var
alternative for it.
- `start.sh` only adds `--dangerously-skip-permissions` for tasks when
no explicit `permission_mode` is set (same fix as #846; included here so
this PR is self-contained, happy to drop if #846 lands first).
- `permission_mode`, `allowed_tools`, and `disallowed_tools` are marked
deprecated and shimmed into `managed_settings.permissions` for one
release when `managed_settings` is not provided.
- README security warning rewritten to point at the policy mechanism
instead of telling people the module is unsafe by design.

## Relationship to #861

#861 strips this module to install-and-configure and removes
`permission_mode` / `allowed_tools` / `disallowed_tools` outright.
`managed_settings` is the natural replacement for those: it is
install-time (survives the `start.sh` removal), it covers everything the
dropped variables did plus `hooks`, `env`, `model`, `apiKeyHelper`, and
the rest of the settings schema, and it does not require the module to
know anything about how Claude is launched. If #861 lands first I will
rebase this on top and drop the deprecation shim and the `start.sh`
hunk.

## Validation

- `terraform fmt` / `terraform validate` clean
- New tests: `claude-managed-settings-written`,
`claude-managed-settings-legacy-shim`,
`claude-no-policy-keys-in-claudejson`, plus an assertion in
`claude-auto-permission-mode` that `--dangerously-skip-permissions` is
absent when a mode is set
- Manually verified `/etc/claude-code/managed-settings.d/*.json`
precedence in the Claude Code CLI source

Closes #818. Relates to #284, #846, #861.

Disclosure: I work at Anthropic on the Claude Code team. Happy to adjust
scope or split this further if that is easier to review.

---------

Co-authored-by: DevCats <chris@dualriver.com>
Co-authored-by: DevCats <christofer@coder.com>
2026-05-15 08:27:42 -05:00

15 KiB

display_name description icon verified tags
Claude Code Install and configure the Claude Code CLI in your workspace. ../../../../.icons/claude.svg true
agent
claude-code
ai
anthropic
ai-gateway

Claude Code

Install and configure the Claude Code CLI in your workspace. Starting Claude is left to the caller (template command, IDE launcher, or a custom coder_script).

module "claude-code" {
  source            = "registry.coder.com/coder/claude-code/coder"
  version           = "5.2.0"
  agent_id          = coder_agent.main.id
  anthropic_api_key = "xxxx-xxxxx-xxxx"
}

Warning

If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for Coder Tasks and Boundary. We plan to add those back in a follow-up. Keep using v4.x.x if you depend on them. See #861 for the full migration guide.

Prerequisites

Provide exactly one authentication method:

  • Anthropic API key: get one from the Anthropic Console and pass it as anthropic_api_key.
  • Claude.ai OAuth token (Pro, Max, or Enterprise accounts): generate one by running claude setup-token locally and pass it as claude_code_oauth_token.
  • Coder AI Gateway (Coder Premium, Coder >= 2.30.0): set enable_ai_gateway = true. The module authenticates against the gateway using the workspace owner's session token. Do not combine with anthropic_api_key or claude_code_oauth_token.

workdir

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.

Examples

Standalone mode with a launcher app

Authenticate Claude directly against Anthropic's API and add a coder_app that users can click from the workspace dashboard to open an interactive Claude session.

locals {
  claude_workdir = "/home/coder/project"
}

module "claude-code" {
  source            = "registry.coder.com/coder/claude-code/coder"
  version           = "5.2.0"
  agent_id          = coder_agent.main.id
  workdir           = local.claude_workdir
  anthropic_api_key = "xxxx-xxxxx-xxxx"
}

resource "coder_app" "claude" {
  agent_id     = coder_agent.main.id
  slug         = "claude"
  display_name = "Claude Code"
  icon         = "/icon/claude.svg"
  open_in      = "slim-window"
  command      = <<-EOT
    #!/bin/bash
    set -e
    cd ${local.claude_workdir}
    claude
  EOT
}

Note

coder_app.command runs when the user clicks the app tile. Combine with anthropic_api_key, claude_code_oauth_token, or enable_ai_gateway = true on the module to pre-authenticate the CLI.

Usage with AI Gateway

AI Gateway is a Premium Coder feature that provides centralized LLM proxy management. Requires Coder >= 2.30.0.

module "claude-code" {
  source            = "registry.coder.com/coder/claude-code/coder"
  version           = "5.2.0"
  agent_id          = coder_agent.main.id
  workdir           = "/home/coder/project"
  enable_ai_gateway = true
}

When enable_ai_gateway = true, the module sets:

  • ANTHROPIC_BASE_URL to ${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic
  • ANTHROPIC_AUTH_TOKEN to the workspace owner's Coder session token

Claude Code then routes API requests through Coder's AI Gateway instead of directly to Anthropic.

Caution

enable_ai_gateway = true is mutually exclusive with anthropic_api_key and claude_code_oauth_token. Setting any of them together fails at plan time.

Enterprise policy via managed settings

The managed_settings input writes a policy file to /etc/claude-code/managed-settings.d/10-coder.json inside the workspace. Claude Code reads this directory at startup with the highest configuration precedence, so users cannot override these values in their own ~/.claude/settings.json. This is a local file mechanism and works with any inference backend (Anthropic API, AWS Bedrock, Google Vertex AI, or AI Gateway).

module "claude-code" {
  source            = "registry.coder.com/coder/claude-code/coder"
  version           = "5.2.0"
  agent_id          = coder_agent.main.id
  workdir           = "/home/coder/project"
  anthropic_api_key = "xxxx-xxxxx-xxxx"

  managed_settings = {
    permissions = {
      defaultMode                  = "acceptEdits"
      disableBypassPermissionsMode = "disable"
      deny                         = ["Bash(curl:*)", "Bash(wget:*)", "WebFetch"]
    }
    env = {
      DISABLE_TELEMETRY = "0"
    }
  }
}

See the Claude Code settings reference for the full schema. Common keys: permissions (defaultMode, allow, deny, disableBypassPermissionsMode, additionalDirectories), env, model, apiKeyHelper, hooks, cleanupPeriodDays.

Advanced Configuration

This example shows version pinning, a pre-installed binary path, a custom model, and MCP servers.

module "claude-code" {
  source   = "registry.coder.com/coder/claude-code/coder"
  version  = "5.2.0"
  agent_id = coder_agent.main.id
  workdir  = "/home/coder/project"

  anthropic_api_key = "xxxx-xxxxx-xxxx"

  claude_code_version = "2.0.62" # Pin to a specific Claude CLI version.

  # Skip the module's installer and point at a pre-installed Claude binary.
  # claude_binary_path can only be customized when install_claude_code is false.
  install_claude_code = false
  claude_binary_path  = "/opt/claude/bin"

  model = "sonnet"

  mcp = <<-EOF
  {
    "mcpServers": {
      "my-custom-tool": {
        "command": "my-tool-server",
        "args": ["--port", "8080"]
      }
    }
  }
  EOF

  mcp_config_remote_path = [
    "https://gist.githubusercontent.com/35C4n0r/cd8dce70360e5d22a070ae21893caed4/raw/",
    "https://raw.githubusercontent.com/coder/coder/main/.mcp.json"
  ]
}

Note

Swap anthropic_api_key for claude_code_oauth_token = "xxxxx-xxxx-xxxx" to authenticate via a Claude.ai OAuth token instead. Pass exactly one.

Note

Servers configured through mcp or mcp_config_remote_path are added at Claude Code's user scope, making them available across every project the workspace owner opens. For project-local MCP servers, commit a .mcp.json to the project repository instead.

Note

Remote URLs should return a JSON body in the following format:

{
  "mcpServers": {
    "server-name": {
      "command": "some-command",
      "args": ["arg1", "arg2"]
    }
  }
}

The Content-Type header doesn't matter, both text/plain and application/json work fine.

Serialize a downstream coder_script after the install pipeline

The module exposes the coder exp sync name of each script it creates via the scripts output: an ordered list (pre_install, install, post_install) of names for scripts this module actually creates. Scripts that were not configured are absent from the list.

Downstream coder_script resources can wait for this module's install pipeline to finish using coder exp sync want <self> <each name>:

module "claude-code" {
  source            = "registry.coder.com/coder/claude-code/coder"
  version           = "5.2.0"
  agent_id          = coder_agent.main.id
  workdir           = "/home/coder/project"
  anthropic_api_key = "xxxx-xxxxx-xxxx"
}

resource "coder_script" "post_claude" {
  agent_id     = coder_agent.main.id
  display_name = "Run after Claude Code install"
  run_on_start = true
  script       = <<-EOT
    #!/bin/bash
    set -euo pipefail
    trap 'coder exp sync complete post-claude' EXIT
    coder exp sync want post-claude ${join(" ", module.claude-code.scripts)}
    coder exp sync start post-claude

    # Your work here runs after claude-code finishes installing.
    claude --version
  EOT
}

Usage with AWS Bedrock

Prerequisites

AWS account with Bedrock access, Claude models enabled in Bedrock console, and appropriate IAM permissions.

Configure Claude Code to use AWS Bedrock for accessing Claude models through your AWS infrastructure.

resource "coder_env" "bedrock_use" {
  agent_id = coder_agent.main.id
  name     = "CLAUDE_CODE_USE_BEDROCK"
  value    = "1"
}

resource "coder_env" "aws_region" {
  agent_id = coder_agent.main.id
  name     = "AWS_REGION"
  value    = "us-east-1" # Choose your preferred region
}

# Option 1: Using AWS credentials

variable "aws_access_key_id" {
  type        = string
  description = "Your AWS access key ID. Create this in the AWS IAM console under 'Security credentials'."
  sensitive   = true
}

variable "aws_secret_access_key" {
  type        = string
  description = "Your AWS secret access key. This is shown once when you create an access key in the AWS IAM console."
  sensitive   = true
}

resource "coder_env" "aws_access_key_id" {
  agent_id = coder_agent.main.id
  name     = "AWS_ACCESS_KEY_ID"
  value    = var.aws_access_key_id
}

resource "coder_env" "aws_secret_access_key" {
  agent_id = coder_agent.main.id
  name     = "AWS_SECRET_ACCESS_KEY"
  value    = var.aws_secret_access_key
}

# Option 2: Using Bedrock API key (simpler)

variable "aws_bearer_token_bedrock" {
  type        = string
  description = "Your AWS Bedrock bearer token. This provides access to Bedrock without needing separate access key and secret key."
  sensitive   = true
}

resource "coder_env" "bedrock_api_key" {
  agent_id = coder_agent.main.id
  name     = "AWS_BEARER_TOKEN_BEDROCK"
  value    = var.aws_bearer_token_bedrock
}

module "claude-code" {
  source   = "registry.coder.com/coder/claude-code/coder"
  version  = "5.2.0"
  agent_id = coder_agent.main.id
  workdir  = "/home/coder/project"
  model    = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
}

Note

For additional Bedrock configuration options (model selection, token limits, region overrides, etc.), see the Claude Code Bedrock documentation.

Usage with Google Vertex AI

Prerequisites

GCP project with Vertex AI API enabled, Claude models enabled through Model Garden, service account with Vertex AI permissions, and appropriate IAM permissions (Vertex AI User role).

Configure Claude Code to use Google Vertex AI for accessing Claude models through Google Cloud Platform.

variable "vertex_sa_json" {
  type        = string
  description = "The complete JSON content of your Google Cloud service account key file. Create a service account in the GCP Console under 'IAM & Admin > Service Accounts', then create and download a JSON key. Copy the entire JSON content into this variable."
  sensitive   = true
}

resource "coder_env" "vertex_use" {
  agent_id = coder_agent.main.id
  name     = "CLAUDE_CODE_USE_VERTEX"
  value    = "1"
}

resource "coder_env" "vertex_project_id" {
  agent_id = coder_agent.main.id
  name     = "ANTHROPIC_VERTEX_PROJECT_ID"
  value    = "your-gcp-project-id"
}

resource "coder_env" "cloud_ml_region" {
  agent_id = coder_agent.main.id
  name     = "CLOUD_ML_REGION"
  value    = "global"
}

resource "coder_env" "vertex_sa_json" {
  agent_id = coder_agent.main.id
  name     = "VERTEX_SA_JSON"
  value    = var.vertex_sa_json
}

resource "coder_env" "google_application_credentials" {
  agent_id = coder_agent.main.id
  name     = "GOOGLE_APPLICATION_CREDENTIALS"
  value    = "/tmp/gcp-sa.json"
}

module "claude-code" {
  source   = "registry.coder.com/coder/claude-code/coder"
  version  = "5.2.0"
  agent_id = coder_agent.main.id
  workdir  = "/home/coder/project"
  model    = "claude-sonnet-4@20250514"

  pre_install_script = <<-EOT
    #!/bin/bash
    # Write the service account JSON to a file
    echo "$VERTEX_SA_JSON" > /tmp/gcp-sa.json

    # Install prerequisite packages
    sudo apt-get update
    sudo apt-get install -y apt-transport-https ca-certificates gnupg curl

    # Add Google Cloud public key
    curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg

    # Add Google Cloud SDK repo to apt sources
    echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list

    # Update and install the Google Cloud SDK
    sudo apt-get update && sudo apt-get install -y google-cloud-cli

    # Authenticate gcloud with the service account
    gcloud auth activate-service-account --key-file=/tmp/gcp-sa.json
  EOT
}

Note

For additional Vertex AI configuration options (model selection, token limits, region overrides, etc.), see the Claude Code Vertex AI documentation.

Telemetry export (OpenTelemetry)

Claude Code can emit OpenTelemetry metrics and events covering token usage, tool calls, session lifecycle, and errors (see the monitoring docs). Set telemetry.enabled = true and point otlp_endpoint at your OTLP collector.

The module automatically tags every span and metric with coder.workspace_id, coder.workspace_name, coder.workspace_owner, and coder.template_name via OTEL_RESOURCE_ATTRIBUTES, so Claude Code telemetry can be joined directly against Coder's audit logs and exectrace records on workspace_id.

module "claude-code" {
  source            = "registry.coder.com/coder/claude-code/coder"
  version           = "5.2.0"
  agent_id          = coder_agent.main.id
  workdir           = "/home/coder/project"
  anthropic_api_key = "xxxx-xxxxx-xxxx"

  telemetry = {
    enabled       = true
    otlp_endpoint = "http://otel-collector.observability:4317"
    otlp_protocol = "grpc"
    otlp_headers = {
      authorization = "Bearer ${var.otel_token}"
    }
    resource_attributes = {
      "service.name" = "claude-code"
    }
  }
}

Troubleshooting

If you encounter any issues, check the log files in the ~/.coder-modules/coder/claude-code/logs directory within your workspace for detailed information.

# Installation logs
cat ~/.coder-modules/coder/claude-code/logs/install.log

# Pre/post install script logs
cat ~/.coder-modules/coder/claude-code/logs/pre_install.log
cat ~/.coder-modules/coder/claude-code/logs/post_install.log

References