Skip to content
123 changes: 123 additions & 0 deletions docs/src/content/docs/reference/cli/approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
title: apm approve / apm deny
description: Manage the executable approval gate for dependency packages.
sidebar:
order: 25
---

## Synopsis

```bash
apm approve [PACKAGE_REF...] [OPTIONS]
apm deny [PACKAGE_REF...] [OPTIONS]
```

## Description

APM blocks executable primitives (hooks, bin/ executables) from
dependency packages by default. The `allowExecutables` block in
`apm.yml` records which packages have been explicitly approved to
deploy executables.

`apm approve` adds a package to the allowlist. `apm deny` removes it.

### How the gate works

When `apm install` encounters a dependency that ships hooks or bin/
executables:

1. If `allowExecutables` is **absent** from `apm.yml`, everything is
approved (backward-compatible, no gate).
2. If `allowExecutables` is **present** (even empty `{}`), only listed
packages may deploy executables.
3. In interactive mode, `apm install` prompts for each unapproved
package. In CI (non-interactive), unapproved executables cause a
hard error.

Local project content (the root `.apm/` directory) is always trusted.

### What is gated

| Type | Gated | Notes |
|------|-------|-------|
| Hooks (`.apm/hooks/`, `hooks/`) | Yes | Auto-fire in IDE on lifecycle events |
| Bin executables (`bin/`) | Yes | Deployed to agent PATH via symlinks |
| MCP servers | No | Enforcement deferred to a future release |
| Text primitives (skills, agents, instructions) | No | No code execution risk |

## Options

### `apm approve`

| Flag | Description |
|------|-------------|
| `PACKAGE_REF` | One or more packages to approve (e.g. `ci-hooks@acme`). |
| `--pending` | List all packages with unapproved executables. |
| `--all` | Approve all currently blocked packages. |

### `apm deny`

| Flag | Description |
|------|-------------|
| `PACKAGE_REF` | One or more packages to deny (removes from allowlist). |

## Manifest format

Approvals are stored in `apm.yml` under `allowExecutables`, keyed by
`name#version` with per-type boolean flags:

```yaml
allowExecutables:
"ci-hooks@acme#1.2.0":
hooks: true
bin: true
"dev-tools@org#0.5.0":
hooks: true
```

Version pinning means approval must be renewed when a package updates.

## Examples

Approve a specific package:

```bash
apm approve ci-hooks@acme
```

Show all blocked packages:

```bash
apm approve --pending
```

Approve everything (migration helper):

```bash
apm approve --all
```

Revoke approval:

```bash
apm deny ci-hooks@acme
```

## Non-interactive / CI usage

In CI environments (`CI=true`, `APM_NON_INTERACTIVE=1`, or when stdin
is not a TTY), `apm install` fails with exit code 1 if any dependency
has unapproved executables. Pre-approve packages in `apm.yml` before
CI runs:

```bash
# One-time setup: approve all current dependencies
apm approve --all
git add apm.yml
git commit -m "Approve executable dependencies"
```

## See also

- [`apm install`](../install/) -- the install command that enforces the gate
- [`apm audit`](../audit/) -- audit installed packages
2 changes: 2 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,5 @@ Experimental flags MUST NOT gate security-critical behaviour (content scanning,
`apm config set external.<name>.llm true|false` and `apm config set external.<name>.args -- "<flags>"` persist per-scanner external-scanner defaults to `~/.apm/config.json` (JSON section `external_scanners.<name>.{llm,args}`), behind `apm experimental enable external-scanners`. `<name>` is validated against the supported scanners (e.g. `skillspector`). `.args` is shlex-split and stored as a list; use the `--` separator so Click does not parse a leading `--flag` as an option. `apm config get external.<name>.{llm,args}`, `apm config unset external.<name>.{llm,args}`, and `apm config unset external.<name>` (removes both) round out the surface. These keys are reachable only when the flag is enabled; bare `apm config get` lists set external keys when the flag is on. CLI flags (`--external-llm`, `--external-args`) override these values for a single run.

`apm config set mcp-registry-url https://mcp.internal.example.com` persists a private MCP registry URL so users do not need to export `MCP_REGISTRY_URL` every session. Accepts `http://` or `https://` URLs; all other schemes are rejected. Resolution order: `--registry <url>` flag on `apm mcp install` / `apm install --mcp` > `MCP_REGISTRY_URL` env var > `mcp-registry-url` in `~/.apm/config.json` > built-in public default. When the config layer is active, `apm mcp search` prints a `Registry (config): <url>` diagnostic. `apm config unset mcp-registry-url` removes the persisted URL.

`apm approve [PACKAGE_REF...]` adds packages to the `allowExecutables` block in `apm.yml`, permitting their hooks and bin/ executables to deploy during `apm install`. `apm approve --pending` lists all packages with unapproved executables. `apm approve --all` approves every currently blocked package. `apm deny [PACKAGE_REF...]` removes packages from the allowlist, blocking their executables on the next install. Approvals are version-pinned (`name#version`); updating a package requires re-approval. In non-interactive environments (CI), unapproved executables cause `apm install` to exit 1.
3 changes: 3 additions & 0 deletions src/apm_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
_check_and_notify_updates,
print_version,
)
from apm_cli.commands.approve import approve_cmd, deny_cmd
from apm_cli.commands.audit import audit
from apm_cli.commands.cache import cache
from apm_cli.commands.compile import compile as compile_cmd
Expand Down Expand Up @@ -147,8 +148,10 @@ def cli(ctx, verbose: bool) -> None:


# Register command groups
cli.add_command(approve_cmd, name="approve")
cli.add_command(audit)
cli.add_command(cache)
cli.add_command(deny_cmd, name="deny")
Comment thread
sergio-sisternes-epam marked this conversation as resolved.
cli.add_command(deps)
cli.add_command(view_cmd)
# Hidden backward-compatible alias: ``apm info`` → ``apm view``
Expand Down
Loading
Loading