Installer hook
The hook is one POSIX shell script. It runs after a successful install, finds CLItrail receipts in local browser storage and reports them with a few coarse facts about the machine. It installs nothing, needs no runtime and never changes your installer’s exit status.
#Get the script
Every install path has its own copy, with the service address, website ID and install path ID written in. Either fetch it at install time:
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 || true
or download clitrail.sh for that path from the Setup tab and ship it with your package. The download contains your public service address, website ID and install path ID, never a secret. The website-level address without /paths/… still works: its installs count toward your first install path. You can read the generic source at /report.sh.
#What it does
- Exits immediately, reading nothing, when an opt-out is set or it runs in CI.
- Checks its tools, options and service address. Anything missing or invalid ends the run quietly.
- Prints one line to stderr when stderr is a terminal, so the person installing knows what runs and how to stop it.
- Lists the storage folders of supported browsers for the current user, then scans files of 80 to 4,096 bytes for valid CLItrail markers for your website.
- Notes the CPU architecture, the default browser’s family, the time zone and the language (below).
- Creates or reuses a random installation ID in its state folder.
- Posts one small JSON report to
/v1/events, with curl or GNU wget. - Exits with status 0.
The notice reads:
clitrail: linking this install to your recent visit to https://acme.dev (DO_NOT_TRACK=1 turns this off) — https://YOUR_SERVICE/installer
It names your primary website origin, or the website ID when there is none, and links to the service’s installer page, which explains the hook to the people who see it. CLITRAIL_QUIET=1 hides it.
#Options
| Option | Purpose |
|---|---|
--event NAME | install_completed (default), first_run or install_started. See First-run events. |
--install-path ID | Reports toward another install path (ip_…) than the one written in. Wins over CLITRAIL_INSTALL_PATH. |
--installation-scope NAME | Separates independent installations of your products on one machine. Default default. |
--event-id ID | 8 to 128 letters, digits, _ or -. Random by default. Reusing an ID makes a retry idempotent. |
--handoff TOKEN | Reports a handoff token instead of reading browser storage. |
--doctor | Prints diagnostics. Sends nothing. |
--dry-run | Scans and prints a JSON summary, such as {"roots":3,. Sends nothing and writes no state. |
--state-dir DIR | Where the installation ID is kept. |
--root DIR | Scans only this browser profile or folder instead of discovering them. For development and tests. |
--url-id ID | Accepts only markers for this page. |
--endpoint URL, --project ID | Override the service address and website ID written into the script. |
An unknown option or a missing value ends the run silently, so a typo can never fail an install. --doctor says what was wrong.
#Environment variables
| Variable | Effect |
|---|---|
DO_NOT_TRACK | Any value except empty, 0 or false turns the hook off. It exits before reading anything. |
CLITRAIL_DISABLE | Same as DO_NOT_TRACK, for CLItrail only. |
CI, GITHUB_ACTIONS, GITLAB_CI, BUILDKITE, CIRCLECI, TF_BUILD, JENKINS_URL | CI detected: the hook exits before reading anything. CI counts unless it is false or 0; the others count when set at all. |
CLITRAIL_ALLOW_CI=1 | Reports even in CI. |
CLITRAIL_QUIET=1 | Hides the stderr notice. |
CLITRAIL_INSTALL_PATH | An install path ID to report toward, instead of the one written into the script. |
CLITRAIL_HANDOFF | A handoff token, as with --handoff. Set by the handoff command for hooks bundled in your installer. |
CLITRAIL_INSTALLATION_SCOPE | Default for --installation-scope. |
CLITRAIL_STATE_DIR | Default for --state-dir. Otherwise ~/.local/state/clitrail. Older hooks defaulted to ~/.local/: without this variable or --state-dir, a hook that has no ID yet copies a valid one from there, so an install already reported keeps its installation ID. |
#Where it looks
| System | Browsers | Folders |
|---|---|---|
| macOS | Chrome (and Beta, Dev, Canary), Chromium, Edge, Brave, Vivaldi | ~/Library/ |
| macOS | Firefox | ~/Library/ |
| Linux | Chrome (and Beta, Dev), Chromium, Edge, Brave, Vivaldi | ${XDG_CONFIG_HOME:-~/.config}/ |
| Linux | Chromium as a Snap or Flatpak | ~/snap/, ~/.var/ |
| Linux | Firefox, including Firefox 147+ XDG profiles, the Snap and the Flatpak | ~/.mozilla/, ${XDG_CONFIG_HOME:-~/.config}/, ~/snap/, ~/.var/, each */ |
| Linux | Zen, and its Flatpak | ~/.zen, ~/.var/ |
Safari’s protected storage is never searched; use the handoff command. Arc, and Zen on macOS, publish no profile folder, so they are not searched. Symbolic links in these paths are rejected. On other systems the hook reports nothing unless it is given --root or a handoff token. See the support matrix.
Without --root, the hook looks only in the browsers’ OPFS folders above. There it opens files of 80 to 4,096 bytes, which can include other websites’ small OPFS files, and compares each with CLItrail’s marker format for your website. Anything else is discarded: it is never kept, printed, sent or run as code. It never opens history, cookies, passwords, bookmarks or cache.
#What it sends
{
"project": "p_04de14934e8005ed97dd",
"event_id": "6f2da9610246dec311f2b793f7c32902",
"installation_id": "6e1bb354196915f373a389bb92549519",
"event_type": "install_completed",
"receipts": ["5GodFohsqck6lAXWFydmiZTBXvtQnU2WzbpsB8tq-_k"],
"platform": "darwin",
"install_path": "ip_4c1f0a9e3b7d2c6e8f10",
"arch": "arm64",
"browsers": ["chrome", "firefox"],
"default_browser": "chrome",
"timezone": "Europe/Berlin",
"language": "en-US"
}
| Field | Where it comes from |
|---|---|
receipts | The CLItrail receipts found for your website, at most 64. Empty with a handoff, when handoff carries the token. |
platform | darwin or linux from uname -s; absent elsewhere. The raw output is never sent. |
install_path | The install path ID written into the script, or from --install-path or CLITRAIL_INSTALL_PATH. Absent from older scripts. |
arch | arm64 or x86_64 from uname -m. An x86_64 shell on Apple silicon under Rosetta reports arm64, from sysctl.proc_translated. |
browsers | The families of supported browsers whose profile folder exists: existence checks only. Sent only when the hook discovered folders itself. |
default_browser | The family of the default https handler: on macOS from LaunchServices’ preferences (defaults read; with none set, macOS opens Safari), on Linux from xdg-settings or xdg-mime. Each helper gets at most 1 second; a missing answer leaves the field out. |
timezone | An IANA zone name from TZ, the /etc/localtime link or /etc/timezone. |
language | A language tag from the locale (en_US.UTF-8 → en-US), or on macOS the region setting. |
Browser families, the default browser, the system and the architecture feed the dashboard’s browser and system breakdowns on every plan. The time zone and language are used only by reconstruction on paid plans. No file paths, user names, host names or command output are sent.
#Run as the right user
Receipts live in the visitor’s browser profile, so the hook must run as that OS user on that computer. 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, such as a remote agent, an SSH session or a container, use the handoff command.
#Requirements and limits
- Tools. POSIX
sh,awk,find,mktemp,date,od,tr,mkdir,cat,rm,rmdir,uname, andcurlor GNUwget. No jq, Python, Node or downloaded binary. - Service address. https, or
http://127.0.0.1,localhostor[::1]with a port for local development. - Time and size. Parsing stops after about 4 seconds or 20,000 candidate files, and reports at most 64 receipts. curl waits up to 3 seconds to connect and 5 seconds in total; wget makes one attempt with a 5-second network timeout and follows no redirects.
- User. The person who visited the website, never root. Remote build machines, package sandboxes and containers usually cannot see that person’s browser.
The script is 29.8 KB of shell. Reporting never fails your install: every problem ends the run with status 0.