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
..