Skip to content

feat(cli): add Cursor agent support and agent-aware devcontainer/proxy bootstrap#53

Open
wiacekm wants to merge 6 commits intoVirtusLab:masterfrom
wiacekm:add-cursor-cli-support
Open

feat(cli): add Cursor agent support and agent-aware devcontainer/proxy bootstrap#53
wiacekm wants to merge 6 commits intoVirtusLab:masterfrom
wiacekm:add-cursor-cli-support

Conversation

@wiacekm
Copy link
Copy Markdown

@wiacekm wiacekm commented Apr 22, 2026

Summary

This PR generalizes the sandcat CLI/devcontainer bootstrap to support multiple agents (Claude and Cursor) instead of a Claude-only path, and updates proxy + settings templates accordingly.

Key changes

  • Introduce agent-aware init plumbing in CLI bootstrap and devcontainer setup.
  • Add a dedicated Cursor user settings template (settings-user-cursor.json).
  • Rename the existing generic user settings template to settings-user-claude.json for explicit agent targeting.
  • Replace hardcoded Docker/mitmproxy behavior with agent placeholders in templates.
  • Split mitmproxy logic into agent-specific addons, adding a new Cursor addon.
  • Simplify and harden runtime trust/proxy env handling in app init scripts.
  • Update docs and test coverage for init, composefile generation, user settings, and mitmproxy behavior.

Why

Agent-specific assumptions in bootstrap and template generation made Cursor support brittle and increased maintenance cost. This change establishes a single agent-aware flow, reduces hardcoded behavior, and keeps proxy/trust configuration consistent across supported agents.

Test plan

  • Run CLI init flow for Claude and verify generated settings/template selection.
  • Run CLI init flow for Cursor and verify generated settings/template selection.
  • Build/start devcontainer and confirm agent placeholders render expected Docker/compose behavior.
  • Verify mitmproxy starts with the correct agent addon and expected options.
  • Run bats tests under cli/test and confirm updated suites pass.
  • Smoke-test agent bootstrap in container (env propagation + API key substitution path).

Copilot AI review requested due to automatic review settings April 22, 2026 06:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR generalizes the sandcat CLI + devcontainer bootstrap to support multiple agents (Claude and Cursor) by introducing agent-aware template customization, agent-specific mitmproxy addons, and new user settings templates, with corresponding updates across tests and docs.

Changes:

  • Add agent plumbing (agents.bash) and wire it through init/devcontainer/template customization paths.
  • Introduce Cursor support via new settings template + dedicated mitmproxy addon, plus updated devcontainer/proxy templates with agent placeholders.
  • Update bootstrap scripts, docs, and bats/pytest coverage to validate Cursor/Claude behavior.

Reviewed changes

Copilot reviewed 24 out of 26 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
cli/test/mitmproxy/test_mitmproxy_addon.py Extends mitmproxy addon tests to cover Cursor streaming and placeholder behavior.
cli/test/init/user_settings.bats Adds Cursor user-settings assertions and cursor-default backfill test.
cli/test/init/stacks.bats Adds zig extension mapping coverage.
cli/test/init/regression.bats Adds regression coverage for Cursor devcontainer compose output and mounts.
cli/test/init/init.bats Verifies init accepts cursor agent and updated interactive agent selection.
cli/test/init/extensions.bats Adds tests for agent template customization (Cursor/Claude placeholders).
cli/test/composefile/composefile.bats Adds Cursor config volume tests and Cursor default-mount behavior.
cli/templates/settings-user-cursor.json New Cursor-focused default user settings template (secrets + network allowlist).
cli/templates/settings-user-claude.json New explicit Claude user settings template (renamed from generic).
cli/templates/devcontainer/sandcat/scripts/mitmproxy_addon_cursor.py New Cursor-focused mitmproxy addon with streaming-safe behavior.
cli/templates/devcontainer/sandcat/scripts/mitmproxy_addon_claude.py Minor formatting tweaks to Claude addon.
cli/templates/devcontainer/sandcat/scripts/app-user-init.sh Makes agent bootstrap injectable via __AGENT_USER_INIT__ and fixes HOME handling.
cli/templates/devcontainer/sandcat/scripts/app-init.sh Hardens trust/proxy env handling (adds NODE_OPTIONS and CA env exports) and preserves env into user-init.
cli/templates/devcontainer/sandcat/compose-proxy.yml Parameterizes mitmproxy command/addon via agent placeholders and adds mitmproxy tuning flags.
cli/templates/devcontainer/devcontainer.json Replaces Claude-specific VS Code extension/settings with agent placeholders.
cli/templates/devcontainer/compose-all.yml Replaces hardcoded agent env with an agent environment placeholder block.
cli/templates/devcontainer/Dockerfile.app Replaces Claude install/home prep with agent placeholders.
cli/libexec/init/init Makes init agent-aware for settings template selection and Cursor defaults/help text.
cli/libexec/init/devcontainer Ensures agent template customization runs during devcontainer generation.
cli/lib/stacks.bash Fixes zig extension mapping (but stack validation list needs updating).
cli/lib/logging.bash Makes color logging best-effort even when tput/TERM is unavailable.
cli/lib/devcontainer.bash Adds customize_agent_templates to render agent placeholders across templates.
cli/lib/composefile.bash Adds Cursor config mounts and agent-aware volume logic in compose customization.
cli/lib/agents.bash New agent abstraction: supported agents, extensions, settings/env/docker/user-init blocks.
cli/README.md Documents Cursor agent support and updates CLI option descriptions.
README.md Adds Cursor CLI documentation and updates trust-store guidance (NODE_OPTIONS + CA env vars).
Comments suppressed due to low confidence (2)

cli/libexec/init/devcontainer:108

  • When --proxy tui is selected, set_proxy_tui_mode rewrites the mitmproxy command. With this PR, the proxy template uses agent-specific addon filenames (e.g. mitmproxy_addon_claude.py / mitmproxy_addon_cursor.py), but set_proxy_tui_mode still hardcodes /scripts/mitmproxy_addon.py, which will fail (and also discards the new --set http2=... etc. options). Update set_proxy_tui_mode to preserve/use the agent-specific addon already in the template (or accept the addon filename as a parameter).
	customize_agent_templates "$devcontainer_dir" "$agent"

	if [[ "$proxy_mode" == "tui" ]]; then
		set_proxy_tui_mode "$devcontainer_dir/sandcat/compose-proxy.yml"
	fi

cli/README.md:20

  • The --stacks help text lists supported stacks but doesn't mention zig, while the code/tests now include a zig stack extension mapping. Once stack validation is updated to actually allow zig, consider adding it to this list (or otherwise remove the zig mapping/tests) to keep CLI docs consistent with supported stacks.
- `--agent` - Agent type: `claude`, `cursor` (skips prompt)
- `--ide` - IDE for devcontainer mode: `vscode`, `jetbrains`, `none` (skips prompt)
- `--stacks` - Comma-separated development stacks to install: `node`, `python`, `java`, `rust`, `go`, `scala`, `ruby`, `dotnet` (skips prompt)
- `--proxy` - Proxy UI mode: `web` (default, mitmweb browser UI) or `tui` (mitmproxy console, use with `sandcat proxy` to attach)
- `--features` - Comma-separated optional features: `tui` (proxy console mode), `1password` (1Password secret resolution via `op` CLI)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cli/lib/stacks.bash
Comment on lines 29 to 33
scala) echo "scalameta.metals" ;;
ruby) echo "shopify.ruby-lsp" ;;
dotnet) echo "ms-dotnettools.csdevkit" ;;
dotnet) echo "ziglang.vscode-zig" ;;
zig) echo "ziglang.vscode-zig" ;;
*) echo "" ;;
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stack_extension/stack_mise_cmd now handle zig, but validate_stacks uses STACK_NAMES, which currently omits zig. As a result sandcat init --stacks zig will still be rejected as invalid. Add zig to STACK_NAMES (and keep any docs/tests in sync) so the new stack is actually usable.

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
Comment on lines +475 to +476
- `SANDCAT_MOUNT_CURSOR_CONFIG=true` mounts `~/.cursor/AGENTS.md` and
`~/.cursor/rules` into the agent container.
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs say SANDCAT_MOUNT_CURSOR_CONFIG=true mounts only ~/.cursor/AGENTS.md and ~/.cursor/rules, but the compose generation also mounts ~/.cursor/skills by default. Either document the additional mount here or remove it from the default Cursor config volumes to avoid surprising host directory creation.

Suggested change
- `SANDCAT_MOUNT_CURSOR_CONFIG=true` mounts `~/.cursor/AGENTS.md` and
`~/.cursor/rules` into the agent container.
- `SANDCAT_MOUNT_CURSOR_CONFIG=true` mounts `~/.cursor/AGENTS.md`,
`~/.cursor/rules`, and `~/.cursor/skills` into the agent container.

Copilot uses AI. Check for mistakes.
Comment thread cli/test/composefile/composefile.bats Outdated
Comment on lines +52 to +63
@test "add_cursor_config_volumes adds AGENTS.md and rules" {
add_cursor_config_volumes "$COMPOSE_FILE"

run yq '.services.agent.volumes | length' "$COMPOSE_FILE"
assert_output "4"

# shellcheck disable=SC2016
yq -e '.services.agent.volumes[] | select(. == "${HOME}/.cursor/AGENTS.md:/home/vscode/.cursor/AGENTS.md:ro")' "$COMPOSE_FILE"

# shellcheck disable=SC2016
yq -e '.services.agent.volumes[] | select(. == "${HOME}/.cursor/rules:/home/vscode/.cursor/rules:ro")' "$COMPOSE_FILE"
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test name says it adds only AGENTS.md and rules, but the length assertion (4) implies an additional volume is expected (currently .cursor/skills). Consider either renaming the test to reflect all mounts or asserting the extra mount explicitly so the intent is clear.

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
Comment on lines +480 to +484
store (which includes the mitmproxy CA) instead of its built-in roots. As
an additional fallback, `app-user-init.sh` patches the `cursor-agent`
launcher script to pass `--use-openssl-ca` directly to the Node.js
invocation. This allows mitmproxy to intercept Cursor API traffic and
perform `SANDCAT_PLACEHOLDER_CURSOR_API_KEY` substitution transparently.
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section says app-user-init.sh patches the cursor-agent launcher script to pass --use-openssl-ca, but the current Cursor bootstrap block only writes Cursor CLI config (cli-config.json) and I don't see any launcher-script patching in the templates/scripts. Either update the docs to match what’s actually done, or add the referenced patching logic so Cursor TLS behavior matches the documentation.

Suggested change
store (which includes the mitmproxy CA) instead of its built-in roots. As
an additional fallback, `app-user-init.sh` patches the `cursor-agent`
launcher script to pass `--use-openssl-ca` directly to the Node.js
invocation. This allows mitmproxy to intercept Cursor API traffic and
perform `SANDCAT_PLACEHOLDER_CURSOR_API_KEY` substitution transparently.
store (which includes the mitmproxy CA) instead of its built-in roots.
When Cursor honors that environment setting, mitmproxy can intercept Cursor
API traffic and perform
`SANDCAT_PLACEHOLDER_CURSOR_API_KEY` substitution transparently. No
launcher-script patching is currently performed by the template bootstrap.

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated

Cursor CLI support is available via `sandcat init --agent cursor`.

- The current template uses temporary compatibility defaults for auth/network.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it would be good to write what these are exactly - to be upfront with the users for anything security-related. I'd be curious, as well ;)

Cursor-focused mitmproxy addon: network policy + secret substitution.

This variant keeps Cursor Connect streaming traffic opaque (no body mutation)
while still applying placeholder substitution in URL/headers/basic auth.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can somehow add the conditiona cursor logic to the original script? having two copies seems like a maintenance problem going forward

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I thought about doing it in separate, but maybe it would be better to do it as a part of this PR.

@adamw
Copy link
Copy Markdown
Member

adamw commented Apr 22, 2026

Claude's review using my review agents:

Correctness
                                                                                                                                                                                
  Critical — TUI proxy mode is broken for both agents. cli/lib/composefile.bash:98 (set_proxy_tui_mode) rewrites the mitmproxy command to -s /scripts/mitmproxy_addon.py, but
  that file no longer exists (renamed to mitmproxy_addon_claude.py / _cursor.py). Any user running sandcat init --proxy tui — including existing Claude users after upgrade —   
  gets a mitmproxy with no addon loaded: no secret substitution → agent receives the placeholder as its API key → 401s. The rewrite also drops the new --set http2=… 
  stream_large_bodies=1m connection_strategy=lazy anticomp=true timeout_read=300 flags, breaking Cursor streaming in TUI mode regardless.

  Medium.
  - cli/lib/composefile.bash:53-54 — add_cursor_config_volumes unconditionally mounts ~/.cursor/AGENTS.md, rules, skills. If absent on the host, Docker auto-creates them as
  root-owned dirs in the user's $HOME.
  - app-init.sh:86 switched su - vscode → su -m vscode, so ~/.profile is no longer sourced. Compensated for mise, but breaks user-customized PATH/NVM/direnv for existing Claude
   users.
  - mitmproxy_addon_claude.py:4 docstring still references the old mitmproxy_addon.py filename.
  - cli/libexec/init/init:29 fallback to settings-user.json is dead (file was renamed).
  - Cursor addon substitutes body placeholders only for textual content types, regressing behavior for application/octet-stream relative to Claude.

  Security

  HIGH — secret leak into the agent container. mitmproxy_addon_cursor.py:244-248 (_write_secrets_json) writes resolved secrets in plaintext to
  /home/mitmproxy/.mitmproxy/sandcat-secrets.json. compose-proxy.yml:30 maps that path into the shared mitmproxy-config volume, which compose-all.yml:20 mounts read-only into
  the agent container at /mitmproxy-config. Any in-container process (including the agent itself under prompt injection) can read CURSOR_API_KEY, GITHUB_TOKEN, etc. chmod 0600
  doesn't help — the file is readable across the volume. This directly breaks sandcat's "real key never enters the container" invariant. Worse: grep shows nothing reads this
  file — it's dead scaffolding from the reverted "real-key bootstrap" commit.

  Medium. stream_large_bodies=1m is now set for Claude too, so request bodies >1MB bypass flow.request.content-based placeholder leak detection — defence-in-depth regression on
   the existing Claude path.

  Low. _is_cursor_streaming_request is client-triggerable via content-type: application/connect+proto, skipping body substitution. No secret disclosure, but the escape hatch is
   client-controlled.

  Checked, fine: allowlist scope (narrow, no stray wildcards), shell/template injection (agent name is validated against claude|cursor before any substitution), TLS trust (uses
   Debian CA store, no NODE_TLS_REJECT_UNAUTHORIZED=0 or CA widening), debug logging (SHA-256 fingerprints only), Claude addon is a pure rename.

  Conciseness / Duplication

  Major duplication between addons. mitmproxy_addon_cursor.py (448 lines) clones ~180 lines from mitmproxy_addon_claude.py (271 lines): SETTINGS_PATHS, _merge_settings,
  _configure_op_token, _resolve_secret_value, _shell_escape, _validate_env_name, _write_placeholders_env, _load_secrets, _load_network_rules, dns_request, etc. Extracting a
  sandcat_mitm_common.py base would shrink Cursor to ~150 lines.

  Repetition in devcontainer.bash:124-182. Four near-identical placeholder-replacement loops in customize_agent_templates — one replace_placeholder_line helper over a (file,
  token, replacement) list would halve it.

  Inconsistent dispatch in agents.bash. sct_agent_compose_environment_block returns a full environment: block; peers return inner content only. This forces a special-case in
  devcontainer.bash:141-152. Normalize to inner-content and put the YAML key in the template.

  Hard-coded agent names in init. init:302-304 (if [[ "$agent" == "cursor" ]]) and the onepassword branch in the next-steps block defeat the agents.bash abstraction.

  Dead code: the entire SANDCAT_SECRETS_PATH / _write_secrets_json path (also a security finding above).

  Tests

  - Addon tests are thin — 4 Cursor-specific cases against a 448-line addon. Uncovered: _is_cursor_host boundaries (e.g. notcursor.sh), path-based streaming detection,
  basic-auth placeholder decode/replace (~40 lines, zero coverage), _normalize_authorization_header_value, _is_textual_content_type edge cases, responseheaders negative path,
  DNS handler on CursorSandcatAddon, leak detection for streaming-to-disallowed-host.
  - Weakened assertion — user_settings.bats bulk-switched assert_output "X" → assert_output --partial "X", which hides regressions. Unrelated to Cursor support.
  - Soft regression check — regression.bats asserts environment // null == null; an accidental empty environment: {} (which compose rejects — exactly the bug the split is meant
   to prevent) would pass. Assert key absence or that docker compose config parses.
  - Unfocused test — extensions.bats "sets claude mitmproxy defaults" asserts 5 facts; only 2 are agent-driven. The others pass regardless of customize_agent_templates.
  - Redundancy — the two customize_devcontainer_extensions removes placeholder… cases duplicate each other; the composefile cursor-volume test duplicates
  add_cursor_config_volumes adds AGENTS.md and rules.
  - No direct tests for agents.bash (209 lines) — exercised only indirectly.

  Priority fixes before merge

  1. Fix composefile.bash:98 TUI path (filename + lost --set flags). Blocks existing Claude users.
  2. Remove _write_secrets_json and SANDCAT_SECRETS_PATH — dead code that actively leaks secrets.
  3. Revert the --partial weakening in user_settings.bats and tighten the environment: regression assertion.
  4. Extract a shared mitm addon module to de-duplicate ~180 lines.
  5. Decide on the su -m behavior change and document it, or restore login-shell semantics for Claude users.

Michał Wiącek added 6 commits April 24, 2026 06:49
… fixes

- Add agent-specific mitmproxy addons (cursor streaming-safe substitution,
  claude variant) and trim shared mitmproxy_addon.py responsibilities
- Wire Cursor bootstrap (sandcat-secrets.json, auth.json, CURSOR_API_KEY
  override, CURSOR_USE_HTTP1_FOR_AGENT → cli-config) via agents.bash and
  devcontainer templates
- Harden mitmproxy service command: http2 profile placeholder, set
  stream_large_bodies=1m (avoid 1-byte threshold streaming tiny POST bodies)
- Extend init/composefile/devcontainer tests, mitmproxy addon pytest
Remove Cursor startup logic that read sandcat-secrets.json and wrote auth/env overrides. Keep placeholder-based CURSOR_API_KEY flow with proxy-side substitution and update related init test expectations.
Install GitHub CLI from the official apt repository, auto-register mounted workspaces as safe git directories, and align Cursor/stack docs and tests with current behavior. Also preserve agent-specific mitmproxy addon flags when switching proxy mode to TUI.

Made-with: Cursor
- Updated README to clarify temporary compatibility defaults for Cursor, including auth passthrough, HTTP/1 compatibility, and tuned proxy command defaults.
- Adjusted `set_proxy_tui_mode` to ensure correct command substitution for mitmproxy.
- Modified devcontainer script to enable HTTP/2 for Cursor.
- Improved user settings template path handling in init script to provide clearer error messaging.
- Refined app-init and app-user-init scripts for better environment variable handling.
- Removed unused secrets handling in Cursor addon to streamline configuration.

These changes aim to improve the stability and usability of the Cursor integration within the Sandcat environment.
@wiacekm wiacekm force-pushed the add-cursor-cli-support branch from d37dfda to 91026b4 Compare April 24, 2026 07:08
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.

3 participants