CLItrail Docs

Install paths

An install path is one way your CLI gets installed: an install script, an npm package, a Homebrew tap. Each has its own hook, so the dashboard shows which paths people install through. The public install command stays unchanged.

#Paths in your organisation

Install paths belong to your organisation and are listed in the dashboard under Install paths. Each has a name, a type (shell, npm, npx, homebrew, python, binary or other) and an ID: ip_ and 20 hex characters. Setup shows each path’s own hook: its one-liner and its clitrail.sh download both carry the path ID, so every install it reports counts toward that path, and Overview and Install events can filter and break down installs by path.

PlanInstall pathsHow to use them
Free1One hook everywhere your CLI reports.
Standard1One hook everywhere your CLI reports.
Enterprise10One path per channel worth comparing: install.sh, npm, Homebrew, PyPI, release binaries.
  • Every organisation has at least one path; the first is created with your organisation, named “Install script”. The last path cannot be deleted (install_path_required).
  • A hook without a path ID, such as the website-level /v1/projects/<website>/report.sh or a script from before install paths, counts toward the first path. So does an ID the organisation does not have.
  • A bundled script reports toward another path with --install-path ip_… or CLITRAIL_INSTALL_PATH: the flag wins over the variable, and the variable over the ID written in.
  • Members, admins and owners create and rename paths; admins and owners delete them. Names are unique in the organisation (duplicate_install_path).
  • Creating a path past the plan’s limit answers 402 with install_path_limit. After a downgrade, paths over the limit are paused until the owner picks which to keep: their installs are still counted, but not attributed or sent to destinations.

#Where to call the hook

Call the hook from wherever your installation succeeds: a shell installer, an npm lifecycle script, or your CLI’s first launch.

# After your installation succeeds:
sh clitrail.sh --event install_completed || true

Completed install. Bundle your website’s generated clitrail.sh with the installer. Run it only after installation succeeds. A pre-install call may use install_started, which is not a conversion and reaches webhooks only. Reference

{
  "scripts": {
    "preinstall": "sh scripts/clitrail.sh --event install_started || true",
    "postinstall": "sh scripts/clitrail.sh --event install_completed || true"
  }
}

Install attempt + completion. Include scripts/clitrail.sh in your package. Preserve existing lifecycle commands and report completion after they succeed. npm script settings can block these hooks; first-run reporting is a fallback. These shell examples target macOS and Linux. Reference

import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';

if (process.platform !== 'win32') {
  const hook = fileURLToPath(
    new URL('../scripts/clitrail.sh', import.meta.url)
  );
  spawn('sh', [hook, '--event', 'first_run'], {
    detached: true, stdio: 'ignore'
  }).on('error', () => {}).unref();
}

// Continue your CLI’s normal startup.

First run. Bundle scripts/clitrail.sh alongside your CLI. npx can run cached packages, so report from the CLI entry point. This records first_run, not a new installation on every invocation; the service deduplicates it. Reference

# In your formula’s install method, after building acme:
libexec.install "acme", "clitrail.sh"
(bin/"acme").write <<~SH
  #!/bin/sh
  (sh "#{libexec}/clitrail.sh" --event first_run \
    >/dev/null 2>&1 || true) &
  exec "#{libexec}/acme" "$@"
SH
(bin/"acme").chmod 0755

First run. Adapt acme to your CLI and preserve its existing build steps. The generated launcher uses the formula’s installed path, so Homebrew’s bin symlink still works. Reporting runs at first launch, not during bottle construction. Reference

from pathlib import Path
import subprocess

def report_first_run():
    try:
        subprocess.Popen(
            ['sh', str(Path(__file__).with_name('clitrail.sh')),
             '--event', 'first_run'],
            stdin=subprocess.DEVNULL,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            start_new_session=True,
        )
    except OSError:
        pass

# Call from your CLI entry point, not on module import.
report_first_run()

First run. Include clitrail.sh as package data and invoke it from your console entry point. PyPI is a registry; wheel installation has no general package-defined post-install hook. Avoid setup.py/build hooks, which run in the wrong lifecycle. Reference

DistributionCall it fromEventKeep in mind
Shell installerAfter every install step succeededinstall_completedA call before installing can only report install_started.
npmpreinstall and postinstallAttempt and completionnpm settings can disable scripts. Keep existing scripts and their failure status.
npx / npm execYour CLI’s entry pointfirst_runCached runs are not new installs; the service keeps one first run per installation.
HomebrewA launcher your formula writesfirst_runBottles are built elsewhere: report from the launcher, at first launch.
Python / PyPIYour console entry pointfirst_runWheels have no general post-install hook.
Go, Rust and other binariesFirst launchfirst_runNothing is bundled: run the path’s one-liner in the background, below.
Docker, CI, remote serversNot applicablenoneThe hook cannot see a browser there; CI runs are skipped by design.

#npm with an existing postinstall

Never overwrite a lifecycle command, and never let reporting hide a failed build. Group the optional part:

{
  "scripts": {
    "postinstall": "npm run build && (sh scripts/clitrail.sh || true)"
  }
}

The hook reports install_completed by default. Add scripts/clitrail.sh to the package’s files list if you use one. The shell hook covers macOS and Linux; call a platform-aware entry point for packages that also run on Windows.

#Homebrew

The Homebrew example above reports a first run from a launcher your formula writes: the launcher starts the hook in the background with --event first_run, then runs your CLI, so reporting never delays or fails it. When the formula installs a package whose own entry point already reports, such as a Python or npm package, no launcher is needed: ship clitrail.sh inside that package. To count those installs under a separate Homebrew path on Enterprise, set CLITRAIL_INSTALL_PATH to that path’s ID in the environment the formula runs the entry point with.

#Go, Rust and other single binaries

There is no package to bundle a script in, so run the path’s one-liner in the background on first launch, and never on Windows. For example, in Go:

if runtime.GOOS != "windows" {
	cmd := exec.Command("sh", "-c", "curl -fsS --connect-timeout 3 --max-time 10 https://YOUR_SERVICE/v1/projects/YOUR_WEBSITE_ID/paths/YOUR_INSTALL_PATH_ID/report.sh 2>/dev/null | sh -s -- --event first_run")
	_ = cmd.Start() // do not wait; never fail startup
}

Keep a small “reported” flag in your CLI’s config folder so it does not call out on every launch; the service keeps one report per installation and event type either way. If you ship an install.sh, for example with GoReleaser, call the hook from there instead.

#Docker, CI and remote servers

The hook cannot see a browser inside a container, a CI runner or a server reached over SSH, and CI runs are skipped by design. For people who install in those places, show the handoff command on your install page. On paid plans, reconstruction can still find a probable match when the install reaches CLItrail from the same public network as the visit, which a remote server usually does not.

#Run as the right user

Whatever the path, the hook must run as the person who visited the website, on that computer: receipts live in their browser profile. If your installer needs root, do the privileged steps with sudo, then run the hook as the original user; a hook running as root finds nothing. For installs that happen elsewhere, show the handoff command. See the installer hook.