Skip to content

fix(workspace files API): write claude-code config to /configs, sudo for root-owned base - #2769

Merged
HongmingWang-Rabbit merged 1 commit into
stagingfrom
fix/workspace-config-write-path
May 4, 2026
Merged

HongmingWang-Rabbit merged 1 commit into
stagingfrom
fix/workspace-config-write-path

Conversation

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor

User-visible bug

```
PUT /workspaces//files/config.yaml
→ 500 {"error":"failed to write file: ssh install: exit status 1
(install: cannot create directory '/opt/configs': Permission denied)"}
```

Hits any claude-code workspace when canvas tries to save config.yaml or do Save & Restart from the Config tab.

Root cause (two compounding bugs)

1. Path mismatch. `claude-code` wasn't in `workspaceFilePathPrefix`, so it fell through to the `/opt/configs` default. But cloud-init at `provisioner/userdata_containerized.go:155` only runs `mkdir -p /configs`. The container bind-mount is `host:/configs → container:/configs`. So `/opt/configs`:

  • doesn't exist on workspace EC2s
  • isn't visible to the runtime even if it did

2. `/configs` is root-owned. cloud-init runs as root, so the directory is created with root ownership. The SSH-as-ubuntu `install -D` command can't write there without escalation. Hermes wasn't affected because its base path `/home/ubuntu/.hermes` is ubuntu-owned.

Fix

  • Add `claude-code: /configs` to the runtime → base-path map.
  • Flip the default fall-through from `/opt/configs` to `/configs` so future containerized runtimes get it right by default.
  • Leave the pre-existing `langgraph: /opt/configs` and `external: /opt/configs` entries unchanged — no user report on those today, and silently relocating any files those runtimes already wrote would be worse than the current state. Migration audit deferred.
  • Prefix the SSH install command with `sudo -n`. The standard EC2 ubuntu user has passwordless sudo; `-n` (non-interactive) ensures clean failure rather than a hang if that ever changes.

Tests

`TestResolveWorkspaceFilePath_KnownRuntimes` updated:

  • New cases: `claude-code`/`CLAUDE-CODE` (case-insensitive) → `/configs/config.yaml`
  • Empty + unknown runtime now expect `/configs/...` (was `/opt/configs/...`)
  • Existing hermes/langgraph/external rows unchanged, confirming scope of the rename

Verification

  • `go build ./...` clean
  • `go test ./internal/handlers/` green
  • CI green
  • Manual: redeploy workspace-server on the affected tenant and retry the user's Save & Restart on workspace `57fb7043-79a0-4a53-ae4a-efb39deb457f`

Test plan

  • Local handlers test suite passes
  • CI green
  • Live verification on user's affected workspace post-deploy

🤖 Generated with Claude Code

…for root-owned base

Root cause of the user-visible 500 ("install: cannot create directory
'/opt/configs': Permission denied") on PUT
/workspaces/<id>/files/config.yaml:

1. Path map fall-through. claude-code wasn't in workspaceFilePathPrefix,
   so resolveWorkspaceFilePath returned the default `/opt/configs/...`.
   That directory doesn't exist on the workspace EC2 — cloud-init in
   provisioner/userdata_containerized.go runs `mkdir -p /configs` only.
   Even if the SSH write had succeeded at /opt/configs, the docker
   container's bind-mount is host:/configs → container:/configs,
   so the file would have been invisible to the runtime.

2. /configs ownership. cloud-init runs as root, so /configs is
   root-owned. The SSH-as-ubuntu install command can't write into it
   without sudo. Hermes wasn't affected because its base path
   (/home/ubuntu/.hermes) is ubuntu-owned.

Two-line fix:

- Add `claude-code: /configs` to the runtime → base-path map and flip
  the default fall-through from `/opt/configs` to `/configs`. Leave the
  pre-existing langgraph/external entries pointing at /opt/configs
  pending a migration audit (no user report on those today, and
  flipping them would silently relocate any files those runtimes
  already wrote).
- Prefix the remote install command with `sudo -n` so the write
  succeeds under the standard EC2 ubuntu/passwordless-sudo posture.
  `-n` (non-interactive) ensures clean failure if that ever changes,
  rather than a hang waiting for a password prompt.

Tests:
- TestResolveWorkspaceFilePath_KnownRuntimes adds claude-code +
  CLAUDE-CODE coverage and updates the empty/unknown default cases
  to expect /configs. The langgraph/external rows stay green
  (unchanged values), confirming the scope of the rename.

Verification:
- go build ./... clean
- go test ./internal/handlers/ green
- The user-reported bug
  (PUT /workspaces/57fb7043-79a0-4a53-ae4a-efb39deb457f/files/config.yaml
   → 500 EACCES on /opt/configs) is the failure mode this fix addresses
  on both axes (path + sudo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@HongmingWang-Rabbit
HongmingWang-Rabbit added this pull request to the merge queue May 4, 2026
Merged via the queue into staging with commit d866d3a May 4, 2026
22 checks passed
@HongmingWang-Rabbit
HongmingWang-Rabbit deleted the fix/workspace-config-write-path branch May 4, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant