# Handoff command

When the terminal cannot read the browser, the page can prepare an install command that carries an opaque token. Running that command reports the token after a successful install. No browser storage is read.

## When to use it

- Safari, whose storage the hook does not read.
- Windows: run the handoff command from WSL (a POSIX shell). PowerShell and cmd are not supported.
- A visit on a phone or tablet, for an install on a computer.
- A remote agent, SSH session, container or `sudo` environment that runs the command somewhere else.

## Prepare the command

Prepare before the visitor clicks Copy, then show and copy that same command:

```js
const install = await InstallAttribution.prepareInstall({
  command: 'brew install your-org/your-cli',
  mode: 'auto',
  enabled: analyticsAllowed
});
document.querySelector('#install-command').textContent = install.command;
document.querySelector('#copy-install').onclick = () =>
  navigator.clipboard.writeText(install.command);
```

The SDK never touches the clipboard itself. Preparing ahead keeps the browser's requirement that clipboard writes happen during a click, and people who select the text by hand get the same command. If the SDK cannot load, keep showing your plain command.

| Option | Default | Purpose |
| --- | --- | --- |
| `command` | required | Your install command, as trusted POSIX shell code, up to 65,536 characters. |
| `mode` | `auto` | `auto`, `handoff` or `storage`, below. |
| `event` | `install_completed` | Use `first_run` for commands like `npx your-cli`, where a cached run is not a new install. |
| `installationScope` | `default` | Separates products on one website; 1 to 64 letters, digits, `_` or `-`. |
| `enabled` | `true` | `false` returns your command unchanged and issues no token. |
| `attribution` | `InstallAttribution.ready` | The result of your own `init` call when you start the SDK yourself. |

| Mode | Behaviour |
| --- | --- |
| `auto` | Prepares a handoff when browser storage failed, and for Safari, Windows, Android, iOS, iPadOS, ChromeOS and browsers the hook does not read, including Chromium forks that name themselves (such as Opera and Yandex), whose profiles the hook does not search. Otherwise returns your command, which relies on the regular hook. |
| `handoff` | Always prepares a handoff. With the tag in its default storage mode, the handoff carries the tab's stored session source and Google's session attributes. Set `data-mode="handoff"` on the tag, or `mode: 'handoff'` in `init`, to skip the OPFS receipt and all web storage too; the handoff then carries only what the preparing page's URL and referrer show. |
| `storage` | Never prepares a handoff. |

`auto` is a routing hint, not a test of what the terminal can read. JavaScript cannot see a future container, `sudo` switch or remote run; choose `handoff` when those are common for your users.

### The result

`prepareInstall` resolves to `{ ok, mode, reason, command }`, plus `expiresAt` for a handoff. Always display `command`.

| `mode` | Meaning | Typical `reason` |
| --- | --- | --- |
| `handoff` | A wrapped command with a token. | `explicit_handoff`, `handoff_selected`, `safari_requires_handoff`, `platform_requires_handoff`, `unqualified_browser`, `opfs_unavailable` |
| `storage` | Your command; the hook will read the receipt. | none |
| `plain` | Your command, without attribution. | `disabled`, `initialize_first`, `invalid_configuration`, `handoff_unavailable`, `handoff_expired` |

## What the command does

The prepared command is `sh -c '…' clitrail '<your command>' '<hook URL>' '<token>' '<scope>' '<event>'`, with every argument shell-quoted. When run, it:

1. Runs your command with `sh -c`, with `CLITRAIL_HANDOFF` and `CLITRAIL_INSTALLATION_SCOPE` set, so a hook bundled in your installer reports the same token. Interactive aliases and functions are not available.
2. Only if your command exits with 0, downloads your website's hook to a private temporary file and runs it with `--handoff`. A failed download is never executed, and the file is removed.
3. Exits with your command's own status.

No analytics ID or secret ever appears in the command. Make sure your command returns a meaningful status: `curl … | sh` can succeed even when the download failed.

## Tokens

- A token is 256 random bits; the service stores only its hash.
- It expires with the visit it belongs to, 30 days by default. Reloading the page does not invalidate a command already prepared.
- Each installation scope gets its own token, but every scope prepared on one page view refers to the same visit, so the page view counts once.
- The first installation that reports it owns it. Retries and a later first run from that installation work; another installation cannot reuse it.
- An invalid or expired token is recorded as unattributed. It never falls back to [reconstruction](https://clitrail.com/docs/reconstruction).
- On Free, the token is discarded unread, like every receipt: see [No attribution on Free](https://clitrail.com/docs/plans#no-attribution-on-free).

A token proves the command came from a prepared page. It does not prove who ran it, and anyone given the full command carries the same token.
