# Wrap a CLI
{{< tier level="sovereign" >}}

Your workflows already depend on CLIs: `gh` for pull requests, `aws` and `gcloud` for infrastructure, `acli` for Jira. Calling them from a shell string works until an argument is wrong, the output format shifts, or the CLI hangs on an interactive prompt. An adapter fixes that. It exposes a few specific commands as typed actions, runs them with fixed arguments, and turns the CLI's output into a clean JSON result that later steps can map.

## Wrap an existing CLI or API

An adapter is an ordinary user tool whose program calls the CLI for you:

1. Valdr checks the action's input against its schema.
2. Your adapter maps that input to a fixed command, such as `gh repo view <repository> --json nameWithOwner,url`.
3. It runs the CLI without a shell, with a timeout and an output cap.
4. It returns `{"ok":true,"data":...}`, or a clean error that includes the CLI's exit code.

The action's `path` only describes the command in the catalog. It never lets a workflow pass arbitrary arguments.

For example, a `repo-view` action can declare an input object with one required string `repository` (pattern `^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$`, `additionalProperties: false`) and an output object with required string fields `nameWithOwner` and `url`. Add the action to your manifest, then dispatch to a handler like this from your Node program:

```js
import { spawnSync } from 'node:child_process';

function repoView(input) {
  if (!input || typeof input.repository !== 'string' ||
      !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(input.repository)) {
    return { ok: false, error: { code: 'invalid_input', message: 'Expected owner/repository.' } };
  }
  const child = spawnSync('gh', [
    'repo', 'view', input.repository, '--json', 'nameWithOwner,url',
  ], {
    shell: false,
    encoding: 'utf8',
    cwd: process.cwd(),
    env: { ...process.env, GH_PROMPT_DISABLED: '1' },
    timeout: 15000,
    maxBuffer: 65536,
  });
  if (child.error || child.status !== 0) {
    return { ok: false, error: { code: 'cli_failed', message: 'GitHub repository read failed; check host installation, authentication, and repository access.', exitCode: child.status, signal: child.signal } };
  }
  try {
    const data = JSON.parse(child.stdout);
    return { ok: true, data };
  } catch {
    return { ok: false, error: { code: 'invalid_cli_result', message: 'GitHub CLI did not return JSON.' } };
  }
}
```

This is a handler, not a complete tool. Your entry program still reads the request, checks the tool identity, dispatches on `request.action`, and writes one result. When you add it:

- Give the tool a `timeoutSeconds` longer than the child's 15 seconds, and a result limit large enough for the CLI's output.
- Mark this read-only action `sideEffects: read` and `retry: idempotent`.
- For authentication, either sign in to the CLI on the workflow host, or declare `inheritEnv: [GH_TOKEN]` and [supply the token through `~/.valdr/environment.env`](../runtime/#environment-variables) or the launch environment. Never put a token value in the manifest.

{{< callout type="warning" >}}
**Never build a shell string from user input.** Pass each value as a separate argument with `shell: false`, not through `sh -c` or string concatenation.
{{< /callout >}}

Good adapters also:

- Request non-interactive, machine-readable output, such as `--json`.
- Limit list sizes so results stay under the tool's result limit.
- Return clear errors for a missing executable, failed authentication, a timeout, and malformed output.

The same pattern works for HTTP APIs: set request timeouts and response-size limits, map success and error explicitly, and read credentials from the host environment.

## Extend a starter for your needs

The starter adapters in `valdr-tools` are a head start, not a complete wrapper. To add the commands your workflows need:

1. Copy the starter's manifest and runner into your own pack. Give each tool an ID that starts with your pack's `pack` value, such as `my-tools.user.gh`, and update the program's identity checks to match. Include the tool's files in `pack.yaml`.
2. Check the installed CLI's help for the command and flags you need. Add the action and its typed input schema to the manifest, and implement its fixed argument mapping in your program. Don't add a pass-through for arbitrary shell commands.
3. Define the result shape and the action's real side effects. A write needs a deliberate retry and reconciliation plan; don't copy the starters' read-only settings onto it.
4. Test success, invalid input, and CLI failure on the workflow host, using the real CLI and an account you're allowed to use.
5. Bump the tool revision in the manifest and the program's identity checks, then [validate, package, and import your pack](../package-and-run/). Select the new revision in the workflows that need it; existing pins don't change.

You own the actions you add, along with installing, upgrading, and signing in to the CLI on the host. To replace a starter with your own implementation, start from [Build Your First Tool](/valdr/docs/workflows/user-tools/quickstart/).

## Represent a CLI tree honestly

You don't need to wrap an entire CLI to get value. Wrap the handful of commands your workflows actually use, and grow from there.

- **Start with reads.** Begin with bounded read actions whose inputs, outputs, and failures you can verify. Keep destructive, interactive, or streaming commands out, or mark them `execution: unsupported` with a reason, until their contracts and recovery are tested.
- **Check the CLI's help.** Read the commands and flags available in your installed CLI before adding an action. You don't need to run every command to decide which ones to support.
- **Retest after CLI upgrades.** Check that the arguments and returned JSON still match your tool's contract.

## Next step

[Package your adapter](/valdr/docs/workflows/user-tools/package-and-run/), or [start from a ready-made CLI starter](/valdr/docs/workflows/user-tools/starters/#cli-starters).

