Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Security

- `apm install --target copilot` no longer bakes environment variable values into `~/.copilot/mcp-config.json`; placeholders like `${env:VAR}`, `${VAR}`, and legacy `<VAR>` are translated to Copilot CLI's native runtime substitution syntax (`${VAR}`) so secrets stay in the shell environment instead of on disk. Legacy `<VAR>` syntax is auto-translated with a deprecation warning; migrate to `${VAR}` in `apm.yml`. (#1152)

### Changed

- **Explicit, auditable target resolution.** `apm install` and `apm compile` now resolve harness targets in a strict priority chain (`--target` flag > `apm.yml` `targets:` > auto-detect from filesystem signals) and print a one-line `[i] Targets: ... (source: ...)` provenance summary so the chosen path is never silently inferred. Empty repositories with no signal now exit 2 with a teaching message instead of silently defaulting to `copilot`. Adds `apm targets` discovery command and `apm compile --all` flag (deprecates `--target all`). (#1165, closes #1154, closes #1122, closes #1130, closes #518, closes #888, closes #891, closes #650, closes #1056)
Expand Down
17 changes: 17 additions & 0 deletions docs/src/content/docs/guides/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,23 @@ mcp:
apm install --trust-transitive-mcp
```

### Environment variable placeholders

`env`, `headers`, and `args` values may reference environment variables using either of two equivalent forms:

| Syntax | Meaning |
| ------------- | ----------------------------------------------------------- |
| `${VAR}` | Reference to an environment variable named `VAR` |
| `${env:VAR}` | Same as above (VS Code-style prefix, normalized internally) |

How APM materializes a placeholder depends on the target harness:

- **Copilot CLI** (`~/.copilot/mcp-config.json`): the placeholder is preserved as `${VAR}` in the generated config and resolved by Copilot CLI from the host environment at server-start. APM never reads the value, so secrets stay in your shell. Make sure the variable is exported before launching `gh copilot`.
- **VS Code** (`.vscode/mcp.json`): the placeholder is rewritten to VS Code's `${env:VAR}` form and resolved by VS Code at server-start.
- **Other harnesses** (Cursor, Windsurf, OpenCode, Claude Desktop, Gemini, Codex): the placeholder is resolved from the current process environment at install time and the literal value is written into the harness config.

The legacy `<VAR>` syntax is still accepted for backward compatibility but emits a deprecation warning; migrate to `${VAR}` in `apm.yml`.

### Validation

Run `apm install --dry-run` to preview MCP dependency configuration without writing any files. Self-defined deps are validated for required fields and transport values; overlay deps are loaded as-is and unknown fields are ignored.
Expand Down
20 changes: 20 additions & 0 deletions docs/src/content/docs/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,26 @@ When installing into `.claude/commands/`, prompt files with an `input:` front-ma

This transformation only applies to the `claude` target. Other targets receive the prompt content unchanged.

**Copilot CLI: env-var translation in `mcp-config.json`:**

When installing MCP servers for the `copilot` target, env-var placeholders in `apm.yml` are **translated** to Copilot CLI's native runtime substitution syntax (`${VAR}`) instead of being resolved to literal values at install time. This applies to HTTP `headers`, stdio `env` blocks, and stdio `args`.

| `apm.yml` syntax | Written to `~/.copilot/mcp-config.json` |
|---------------------|------------------------------------------|
| `${env:VAR}` | `${VAR}` |
| `${VAR}` | `${VAR}` (passthrough) |
| `<VAR>` | `${VAR}` (with deprecation warning) |
| `${VAR:-default}` | `${VAR:-default}` (passthrough) |
| `${input:foo}` | `${input:foo}` (passthrough) |

Copilot CLI resolves these at server-start from the host environment, so plaintext secrets are never written to disk. After install, `apm` emits an aggregated summary:

- A **security improvement** warning when overwriting a config that previously stored literal env values, listing the affected variable names.
- An **unset env var** warning listing every referenced variable not currently exported in your shell, with a copy-pasteable `export KEY=...` hint.
- A one-line **deprecation** warning when any server still uses the legacy `<VAR>` syntax.

Other targets (`cursor`, `windsurf`, `opencode`, `claude`, `gemini`) continue to resolve env-var placeholders at install time pending per-adapter audits.

**Local `.apm/` Content Deployment:**

After integrating dependencies, `apm install` deploys primitives from the project's own `.apm/` directory (instructions, prompts, agents, skills, hooks, commands) to target directories (`.github/`, `.claude/`, `.cursor/`, etc.). Local content takes priority over dependencies on collision. Deployed files are tracked in the lockfile for cleanup on subsequent installs. This works even with zero dependencies -- just `apm.yml` and `.apm/` content is enough.
Expand Down
10 changes: 6 additions & 4 deletions packages/apm-guide/.apm/skills/apm-usage/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,13 @@ dependencies:
headers:
X-Custom: "value"
# Env-var placeholders in headers/env values:
# ${VAR} or ${env:VAR} -> resolved from host env at install time
# by Copilot (VS Code resolves at runtime;
# Codex passes ${...} through unchanged)
# ${VAR} or ${env:VAR} -> Copilot CLI: preserved as ${VAR} and resolved
# from host env at server-start (no plaintext on disk).
# VS Code: rewritten to ${env:VAR} and resolved at runtime.
# Cursor/Windsurf/OpenCode/Claude/Gemini: resolved at install time.
# Codex: passed through unchanged.
# ${input:<id>} -> VS Code prompts user at runtime
# <VAR> -> legacy Copilot syntax (still supported)
# <VAR> -> deprecated; auto-translated, emits a warning
Authorization: "Bearer ${MY_TOKEN}"
tools: ["repos", "issues"]

Expand Down
6 changes: 6 additions & 0 deletions src/apm_cli/adapters/client/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ class ClaudeClientAdapter(CopilotClientAdapter):
target_name: str = "claude"
mcp_servers_key: str = "mcpServers"

# Claude Desktop / Code's mcp config does NOT support runtime env-var
# substitution -- the value in ``env`` must be a literal string. This
# adapter MUST keep the legacy install-time resolution behaviour.
# See #1152 supply-chain analysis.
_supports_runtime_env_substitution: bool = False

@staticmethod
def _normalize_mcp_entry_for_claude_code(entry: dict) -> dict:
"""Normalize a server entry to Claude Code's on-disk shape.
Expand Down
Loading
Loading