Skip to content
Open
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 docs/src/content/docs/producer/repo-shapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ Claude Code skills target. Authoring rules:
`executables.deny` policy (the legacy `bin_deploy` rule remains a
deprecated alias) -- see the
[policy schema](../../reference/policy-schema/#executables).
- Consumers can pass `--trust-bin` on `apm install` to explicitly
consent (suppresses the trust-posture warning) or `--no-trust-bin`
to skip bin/ deployment for a single invocation. Without either
flag, bin/ deploys with a prominent warning.

## What to read next

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/policy-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ deny).

This realizes Claude Code's "skills-directory plugin" contract: a folder under a skills directory that contains `.claude-plugin/plugin.json` loads as `<name>@skills-dir`, and its root `bin/` is added to the Bash tool's `PATH`. The package's `.claude-plugin/plugin.json` is required for Claude to load the folder as a plugin; APM copies it alongside `bin/` when the package ships one. The contract is Claude-specific, so deployment only targets Claude. Restart Claude Code (or run `/reload-plugins`) after install for new executables to be picked up.

**Security note:** deployed executables are tightened to `0o700` on POSIX systems and placed on Claude Code's `PATH`, so Claude can invoke them without further confirmation. By default, APM mirrors npm's trust model: installing a package implies trusting its declared artifacts, including executables. Use this field to opt out globally or per-package in enterprise environments.
**Security note:** deployed executables are tightened to `0o700` on POSIX systems and placed on Claude Code's `PATH`, so Claude can invoke them without further confirmation. By default, APM mirrors npm's trust model: installing a package implies trusting its declared artifacts, including executables. Use this field to opt out globally or per-package in enterprise environments. Additionally, the `--trust-bin` / `--no-trust-bin` flags on `apm install` provide per-invocation consent: `--trust-bin` suppresses the trust-posture warning, `--no-trust-bin` skips bin/ deployment even if policy allows it, and the default (no flag) deploys with a warning.

**Scope:** bin/ deployment only activates for global (`-g`, user-scope) installs. Project-scope installs do not deploy executables.

Expand Down
2 changes: 1 addition & 1 deletion packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

| Command | Purpose | Key flags |
|---------|---------|-----------|
| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so a project whitelisting `targets: [copilot]` will not write `.cursor/mcp.json` even if `.cursor/` exists; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) |
| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--trust-bin` / `--no-trust-bin` (per-invocation consent for marketplace-plugin bin/ deployment: `--trust-bin` suppresses the trust-posture warning, `--no-trust-bin` skips bin/ even if policy allows; default deploys with a warning), `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so a project whitelisting `targets: [copilot]` will not write `.cursor/mcp.json` even if `.cursor/` exists; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) |
| `apm targets` | Show resolved deployment targets for the current project (Click group; reads filesystem signals; works with or without `apm.yml`) | `--all` also include the `agent-skills` meta-target (only meaningful with `--json`), `--json` machine-readable output. No provenance line is printed (the table is the provenance). |
| `apm uninstall PKGS...` | Remove packages (accepts `owner/repo` or `name@marketplace`) | `--dry-run`, `-g` global |
| `apm prune` | Remove orphaned packages | `--dry-run` |
Expand Down
12 changes: 12 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,18 @@ Deployed executables are placed on Claude Code's `PATH` and invoked
without further confirmation, so use this field to opt out in
environments where plugin executables are not trusted by default.

### CLI consent flag

In addition to policy-level controls, the `--trust-bin` / `--no-trust-bin`
flags on `apm install` give per-invocation consent:

- `--trust-bin` -- explicitly consent to bin/ deployment (suppresses the
trust-posture warning).
- `--no-trust-bin` -- explicitly deny bin/ deployment for this invocation,
even if the project policy allows it.
- Default (neither flag) -- deploy bin/ but emit a prominent warning
advising the user to pass `--trust-bin` for explicit consent.

## Canvas extension trust (experimental)

Behind the `canvas` experimental flag, a package may ship a Copilot CLI canvas
Expand Down
15 changes: 15 additions & 0 deletions src/apm_cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ class InstallContext:
skill_subset: "builtins.tuple[str, ...] | None" = None
skill_subset_from_cli: bool = False
audit_override: str | None = None
trust_bin: bool | None = None


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1106,6 +1107,18 @@ def _handle_mcp_install(
"local-bundle installs; passing --as without a local bundle path is rejected."
),
)
@click.option(
"--trust-bin/--no-trust-bin",
"trust_bin",
default=None,
help=(
"Consent posture for bin/ executable deployment. "
"--trust-bin explicitly approves bin/ deployment (suppresses the "
"trust-posture warning). --no-trust-bin explicitly skips bin/ "
"deployment even when policy allows it. Default (neither flag): "
"deploys bin/ but emits a prominent trust-posture warning."
),
)
@click.option(
"--root",
"root",
Expand Down Expand Up @@ -1156,6 +1169,7 @@ def install( # noqa: PLR0913
refresh,
legacy_skill_paths,
alias,
trust_bin,
root,
):
"""Install APM and MCP dependencies from apm.yml (like npm install).
Expand Down Expand Up @@ -1561,6 +1575,7 @@ def install( # noqa: PLR0913
plan_callback=None,
skill_subset=_skill_subset,
skill_subset_from_cli=bool(skill_names),
trust_bin=trust_bin,
)

apm_count, mcp_count, lsp_count, apm_diagnostics = _install_apm_packages(
Expand Down
2 changes: 2 additions & 0 deletions src/apm_cli/install/exec_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,5 @@ def log_bin_status(
f" |-- bin/ executables skipped (not approved in allowExecutables). "
f"Run 'apm approve {_pkg_label}' to approve."
)
elif skill_result.bin_skipped_reason == "not_trusted":
log_fn(" |-- bin/ executables skipped (--no-trust-bin). Pass --trust-bin to deploy.")
30 changes: 29 additions & 1 deletion src/apm_cli/install/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,22 @@ def _log_canvas_skip(package_name: str, package_info: Any, logger: InstallLogger
)


def _resolve_bin_skip(
bin_approved: bool,
trust_bin: bool | None,
) -> tuple[bool, str | None]:
"""Combine the ``allowExecutables`` gate with the ``--trust-bin`` flag.

Returns ``(skip_bin, bin_skip_reason_override)`` for
``integrate_package_skill``.
"""
if not bin_approved:
return True, "not_approved"
if trust_bin is False:
return True, "not_trusted"
return False, None


def integrate_package_primitives( # noqa: PLR0913
package_info: Any,
project_root: Path,
Expand All @@ -205,6 +221,7 @@ def integrate_package_primitives( # noqa: PLR0913
is_first_party: bool = False,
allow_executables: builtins.dict[str, builtins.dict[str, bool]] | None = None,
dep_target_subset: list[str] | None = None,
trust_bin: bool | None = None,
) -> dict:
"""Run the full integration pipeline for a single package.

Expand All @@ -225,6 +242,11 @@ def integrate_package_primitives( # noqa: PLR0913
whose key appears in the dict with the matching type set to ``True``.
Local project content (``package_name == "_local"``) is always trusted.

When *trust_bin* is ``False`` (``--no-trust-bin``), bin/ deployment
is skipped with reason ``"not_trusted"``. When ``True``
(``--trust-bin``), the trust-posture warning is suppressed. When
``None`` (default), bin/ deploys but a prominent warning is emitted.
Comment on lines +245 to +248

Returns a dict with integration counters and the list of deployed file paths.
"""
from apm_cli.integration.dispatch import get_dispatch_table
Expand Down Expand Up @@ -522,6 +544,10 @@ def _format_target_collapse(paths: list[str], verbose: bool) -> tuple[str, list[
)
_emit_integration_hints(_prim_name, _info, _log_integration)

# Determine effective bin/ skip and reason. The ``allowExecutables``
# gate and ``--trust-bin`` / ``--no-trust-bin`` are independent gates.
_skip_bin, _bin_skip_reason_override = _resolve_bin_skip(_bin_approved, trust_bin)

skill_result = integrators.skill.integrate_package_skill(
package_info,
project_root,
Expand All @@ -532,7 +558,9 @@ def _format_target_collapse(paths: list[str], verbose: bool) -> tuple[str, list[
skill_subset=skill_subset,
scope=scope,
policy=policy,
skip_bin=not _bin_approved,
skip_bin=_skip_bin,
bin_skip_reason_override=_bin_skip_reason_override,
trust_bin=trust_bin,
)
_skill_target_dirs: set = builtins.set()
for tp in skill_result.target_paths:
Expand Down
1 change: 1 addition & 0 deletions src/apm_cli/install/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def _integrate_materialization(
dep_target_subset=dep_ref.target_subset,
ctx=ctx,
allow_executables=_effective_allow(ctx),
trust_bin=getattr(ctx, "trust_bin", None),
)
mutation_keys = (
"prompts",
Expand Down
34 changes: 33 additions & 1 deletion src/apm_cli/integration/skill_integrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,8 @@ def integrate_package_skill(
scope=None,
policy=None,
skip_bin: bool = False,
bin_skip_reason_override: str | None = None,
trust_bin: bool | None = None,
) -> SkillIntegrationResult:
"""Integrate a package's skill into all active target directories.

Expand All @@ -1259,6 +1261,15 @@ def integrate_package_skill(
package ships one. Used by the executable approval gate to
block unapproved bin/ executables while still deploying text
primitives (skills, sub-skills).
bin_skip_reason_override: When *skip_bin* is True, use this
string as ``bin_skipped_reason`` instead of the default
``"not_approved"``. Lets the caller distinguish
``--no-trust-bin`` (``"not_trusted"``) from the
``allowExecutables`` gate.
trust_bin: Tri-state consent flag. ``True`` suppresses the
trust-posture warning; ``False`` is handled upstream by
setting *skip_bin*; ``None`` (default) emits the warning
when bin/ is deployed.

Returns:
SkillIntegrationResult: Results of the integration operation
Expand Down Expand Up @@ -1318,7 +1329,7 @@ def integrate_package_skill(

if package_info.package_type == _PackageType.MARKETPLACE_PLUGIN:
if skip_bin:
bin_skip_reason = "not_approved"
bin_skip_reason = bin_skip_reason_override or "not_approved"
else:
bin_paths, bin_skip_reason = self._deploy_plugin_bin(
package_info,
Expand All @@ -1328,6 +1339,7 @@ def integrate_package_skill(
policy=policy,
force=force,
logger=logger,
trust_bin=trust_bin,
)

# Check if this is a native Skill (already has SKILL.md at root)
Expand Down Expand Up @@ -1443,6 +1455,7 @@ def _deploy_plugin_bin(
policy=None,
force: bool = False,
logger=None,
trust_bin: bool | None = None,
) -> tuple[list[Path], str | None]:
"""Deploy bin/ executables and plugin manifest for a MARKETPLACE_PLUGIN.

Expand All @@ -1463,6 +1476,13 @@ def _deploy_plugin_bin(
systems. The deployed root is user-scoped (~/.claude/skills/), so
tighter-than-0o755 permissions are correct.

When *trust_bin* is ``None`` (no ``--trust-bin`` / ``--no-trust-bin``
flag), a prominent trust-posture warning is emitted after deployment
so the user is aware that executables are on Claude Code's PATH.
When *trust_bin* is ``True`` (``--trust-bin``), the warning is
suppressed. ``False`` is never passed here (handled upstream by
the caller setting ``skip_bin=True``).

Returns ``(deployed_paths, skip_reason)``. ``skip_reason`` is non-None
ONLY when the package ships a bin/ but it could not be deployed for an
actionable reason ("project_scope", "no_claude_target"), so the install
Expand Down Expand Up @@ -1524,6 +1544,18 @@ def _deploy_plugin_bin(
if manifest is not None:
deployed.append(manifest)

# Trust-posture warning: when bin/ is deployed without explicit
# --trust-bin consent, surface a prominent warning so the user
# knows executables are on Claude Code's PATH.
if deployed and trust_bin is None and logger:
canonical = package_info.get_canonical_dependency_string()
logger.progress(
f"[!] Plugin '{canonical}' deploys executables to Claude Code's PATH "
"(invoked without confirmation). "
"Pass --trust-bin to acknowledge, or --no-trust-bin to skip bin/ deployment.",
symbol="warning",
)
Comment on lines +1552 to +1557

return deployed, None

@staticmethod
Expand Down
Loading
Loading