# The hootlens command line and agent setup

`hootlens` is an npm package (`packages/cli`) that makes Hoot Lens automatic once a repository uses it. One command adds the tag, ties the repository to its project, tells coding agents how to work with Hoot Lens evidence, and connects their MCP client. Agents can run it themselves with no prompts.

Status: implemented and covered by tests that run the built CLI against trimmed fixture repositories for every platform below. It has not been run on the pilot sites or any customer repository, and the package is not published to npm yet (see [Publishing](#publishing)). Until it is, run it from a checkout with `node packages/cli/dist/hootlens.mjs` after `npm run build`.

```sh
npx hootlens init              # interactive: shows every change, then asks
npx hootlens status            # is it installed, and is the live site serving it
npx hootlens doctor            # static checks for common mistakes
```

Requires Node 20 or later. The package has no runtime dependencies.

## For coding agents

Run it without prompts and read the JSON:

```sh
npx hootlens init --project <id> --dry-run --json     # plan only, writes nothing
npx hootlens init --project <id> --yes --json         # write
npx hootlens doctor --json
npx hootlens status --json
```

Rules the CLI holds to, so an agent can rely on them:

- Without a terminal, `init` refuses to write unless `--yes` is given, and exits 2 with nothing changed. `--dry-run` always works.
- `--json` prints one object on stdout and nothing else. An error is `{"ok": false, "error": "..."}`.
- The project id is public. Do not invent one. Take it from the person, from `.hootlens.json`, or from `HOOTLENS_PROJECT`.
- It never writes a token or key. It never prints `HOOTLENS_PAT`.
- Running it again changes nothing when nothing is out of date.

Exit codes: `0` success, `1` a check failed or the person declined, `2` wrong usage (a missing project id, an unknown flag, no `--yes`).

## `hootlens init`

| Flag | Meaning |
| --- | --- |
| `--project <id>` | The project id. Otherwise `HOOTLENS_PROJECT`, then `.hootlens.json`, then (signed in) a choice from your projects, then a prompt. |
| `--yes`, `-y` | Write without asking. |
| `--dry-run` | Show what would change and write nothing. |
| `--agents <list>` | `claude`, `cursor`, `vscode`, `codex`, comma separated; or `all`, `none`, `auto`. The default `auto` configures the agents whose folders exist: `.claude`, `CLAUDE.md` or `.mcp.json` for Claude Code, `.cursor` for Cursor, `.vscode` for VS Code, `.codex` for Codex. When none is found nothing is written for agents and the output says so. |
| `--no-tag` | Leave the site's source alone. Still writes `.hootlens.json`, `AGENTS.md` and MCP files. |
| `--site <url>` | Record the live site address in `.hootlens.json` so `status` can check it. |
| `--endpoint <url>` | Add `data-endpoint` to the tag. Only for a workspace with its own API. |
| `--json` | Machine-readable output, no prompts. |
| `--offline` | Make no network calls, including the check that the project id exists. |
| `--cwd <dir>` | Run against another directory. |

### What it does

1. **Detects the platform** from `package.json` and marker files, using the same platform list and placement as [INSTALL.md](INSTALL.md) and the dashboard (`@hootlens/core/install-guides`). Order: Next.js, Nuxt, SvelteKit, Astro, Angular, Remix and React Router, Gatsby, Vite, Shopify theme, WordPress theme, plain HTML. Next.js is the App Router when `app/layout.*` exists, otherwise the Pages Router.
2. **Finds the project**, as in the `--project` row above. With `HOOTLENS_PAT` set it lists your projects through the API. Unless `--offline`, it asks the public config endpoint whether the id exists and warns when it does not.
3. **Adds the tag** once, in the documented place. If a Hoot Lens tag exists anywhere in the source, nothing is added, and a tag for another project is reported and left alone.
4. **Writes `.hootlens.json`**, **adds the AGENTS.md section**, and **writes MCP configuration**.
5. **Prints next steps**: review and commit, deploy, visit the site, run `hootlens status`, and each selected agent's sign-in step.

### Files it touches

| File | When | Change |
| --- | --- | --- |
| The tag file (below) | Unless `--no-tag` or the tag already exists | One insertion. |
| `.hootlens.json` | Always | `{"projectId": "...", "$schema": "https://hootlens.com/schema/hootlens.json"}`, plus `site` with `--site`. Keys you added are kept. |
| `AGENTS.md` | Always | Created if missing. Otherwise the Hoot Lens section is appended, or updated in place between `<!-- hootlens:start -->` and `<!-- hootlens:end -->`. Text outside the markers is never touched. An unmarked `## Hoot Lens` heading, or unbalanced markers, are left alone and reported. |
| `CLAUDE.md` | Only if it exists | The same section, unless the file already imports `@AGENTS.md` or is a link to `AGENTS.md`. `init` never creates `CLAUDE.md`, because Claude Code reads `AGENTS.md`. |
| `.mcp.json` | `claude` | `mcpServers.hootlens`: `{"type": "http", "url": "https://mcp.hootlens.com/mcp?project=<id>"}` |
| `.cursor/mcp.json` | `cursor` | `mcpServers.hootlens`: `{"url": "..."}` |
| `.vscode/mcp.json` | `vscode` | `servers.hootlens`: `{"type": "http", "url": "..."}` |
| `.codex/config.toml` | `codex` | A `[mcp_servers.hootlens]` table with `url = "..."` |

Other servers and keys in the MCP files are kept, and indentation is preserved. An existing `hootlens` entry that points at `mcp.hootlens.com` is updated (for example when the project changes). One that points elsewhere is kept. A JSON file with comments or a syntax error is not rewritten; the output gives the command to run by hand.

### Where the tag goes

| Platform | File | Edit |
| --- | --- | --- |
| Next.js App Router | `app/layout.tsx` (or `src/app/`, `.jsx`, `.js`) | `import Script from "next/script"` and a `<Script strategy="afterInteractive">` before `</body>` |
| Next.js Pages Router | `pages/_document.tsx` (or `src/pages/`) | `<script async>` inside `<Head>`. The file is created from the install guide when missing. |
| Vite | `index.html` | The tag before `</head>` |
| Nuxt | `nuxt.config.ts` (or `.js`, `.mjs`) | An entry in `app.head.script`, extending whatever is already there |
| SvelteKit | `src/app.html` | The tag before `</head>` |
| Angular | `src/index.html` | The tag before `</head>` |
| Astro | The layout in `src/layouts/` | The tag with `is:inline` before `</head>` |
| Remix, React Router | `app/root.tsx` | `<script async>` before `</head>` |
| Gatsby | `gatsby-ssr.js` | An element in `setHeadComponents` from `onRenderBody`. Created when missing. |
| Shopify theme | `layout/theme.liquid` | The tag before `</head>` (never on checkout) |
| Plain HTML | Every `.html` file with a `<head>`, up to 100 | The tag before `</head>` |
| WordPress, Google Tag Manager | Not edited | The output prints the code and where it goes |

If a file does not have the shape the editor expects (no `</head>`, no `defineNuxtConfig`), nothing is written for the tag and the output prints the exact code with where to paste it (`tag.status` is `manual`).
### Privacy

The tag normally goes in a root layout that wraps every route, so the CLI cannot keep it off signed-in pages by placement. Instead it reads route folders (Next.js, Nuxt, SvelteKit, Remix, Astro, Gatsby) and names routes that look private, such as `/dashboard`, `/admin`, `/account`, `/portal` or `/checkout`, and suggests never-record rules to add in the dashboard under Settings, Recording, Pages to record (for example `/dashboard/**`). The tracker applies those rules before the private page is drawn. Capture itself still follows the project's privacy policy and consent mode, and always skips password, payment and one-time-code values, `data-hoot-private` regions, URL fragments and URLs with credentials.

### JSON output

```json
{
  "ok": true,
  "command": "init",
  "dryRun": false,
  "written": true,
  "projectId": "...",
  "projectSource": "flag",
  "platform": { "id": "nextjs", "label": "Next.js (App Router)", "variant": "app", "evidence": ["..."], "guessed": false },
  "tag": { "status": "add", "files": ["app/layout.tsx"] },
  "agents": ["claude"],
  "changes": [{ "path": "app/layout.tsx", "kind": "tag", "action": "modify" }],
  "manual": [],
  "warnings": [],
  "nextSteps": ["..."]
}
```

`tag.status` is `add`, `present`, `manual`, `unsupported` or `skipped`. A change `action` is `create`, `modify`, `unchanged`, `skipped` or `manual`. `written` is true only when a file changed.

## `hootlens status`

Checks, in order: `.hootlens.json`; a tag in the source and its project id; (unless `--offline`) whether Hoot Lens knows the project; the live site's HTML and `Content-Security-Policy` header (from `--url`, or `site` in `.hootlens.json`); and, with `HOOTLENS_PAT`, the hosted install check (`POST /v1/projects/:id/install-verify`, read scope), which adds whether the tag has loaded for real visitors and whether data has arrived. Without a token the hosted check is skipped; an agent connected through MCP can call `check_install` instead. A tag added by script or Tag Manager does not show in the HTML, and the output says so. Exit code 1 when a check fails.

## `hootlens doctor`

Static analysis of the source. It runs nothing and calls no network. Each finding has a file and line.

| Finding | Severity | Meaning |
| --- | --- | --- |
| `tag-missing` | error | Nothing loads `https://hootlens.com/t.js`. |
| `tag-duplicate` | error, warn | Two copies in one file, or the tag in several framework files. Plain HTML sites may have one per page. |
| `project-mismatch` | error | Tags disagree with each other or with `.hootlens.json`. |
| `csp-script`, `csp-connect` | error | A Content-Security-Policy in a meta tag, `_headers`, `netlify.toml`, `vercel.json` or a config file does not allow `https://hootlens.com` in `script-src` or the collector in `connect-src`. |
| `consent-false` | warn | `['consent', false]` or `setConsent(false)` in code that talks to Hoot Lens. It should run only on the click where a visitor declines. |
| `stop-call` | warn | `['stop']` or `Hootlens.stop()`, often from a language switcher or route change. |
| `page-version-effect` | error | `pageVersion` pushed from an effect or lifecycle hook. |
| `page-version-runtime` | warn | `pageVersion` pushed at runtime. Prefer `data-hoot-region` and `data-hoot-version` in markup. |
| `tag-blocking` | warn | No `async`, `defer` or `afterInteractive` next to the script. |
| `private-routes` | warn | Private-looking routes share the tag. |
| `config` | info, error | `.hootlens.json` missing or invalid. |

Limits: it is pattern matching on text, so it can miss a policy built at runtime and can flag code that is correct in context. Treat each finding as a place to look.

## Agent integrations

Everything under `integrations/agents` comes from one source, `integrations/agents/source/guide.md`. `npm run agents:generate` rewrites the copies, and `tests/cli-agent-rules.test.ts` fails when they differ, so the AGENTS.md section, the Cursor rule, the Codex snippet, the skill and the text `init` embeds cannot drift.

The shared text covers what Hoot Lens records, where the tag is, the project id, how to read data through MCP, the experiment workflow, and privacy:

1. Wrap the changed region in `data-hoot-region` and bump `data-hoot-version` when it changes.
2. Never push `pageVersion` from an effect.
3. Keep `data-hoot-id` stable.
4. Mark rotating regions `data-hoot-dynamic`.
5. Call `log_change` once, after deploy.
6. Read `change_impact` later and quote its wording.

### Claude Code plugin

`integrations/agents/claude-plugin` bundles the hosted MCP server (`https://mcp.hootlens.com/mcp`, every project the person can open), the `hoot-lens` skill, and four commands: `/hootlens:weekly-review`, `/hootlens:investigate`, `/hootlens:did-it-help` and `/hootlens:install`. `.claude-plugin/marketplace.json` at the repository root lists it.

```text
/plugin marketplace add Parallel-Platforms/hootlens
/plugin install hootlens@hootlens
```

The plugin and a project `.mcp.json` from `init` point at the same server. Keep one of them if you want a single `hootlens` entry in `/mcp`: the plugin for all projects, or the project file to limit an agent to one project.

### Cursor and Codex

`init` does not write a Cursor rule because Cursor reads `AGENTS.md`. To use a rule file anyway, copy `integrations/agents/cursor/hootlens.mdc` to `.cursor/rules/hootlens.mdc`. It is an agent-requested rule (a description, `alwaysApply: false`). Codex reads `AGENTS.md`; `integrations/agents/AGENTS.snippet.md` is the section without a project id, for repositories that do not use `init`.

### What was verified against official documentation

Checked on 2026-10-07 by fetching each product's current documentation:

| Tool | Verified | Not verified |
| --- | --- | --- |
| Claude Code | Plugin layout (`.claude-plugin/plugin.json`, `skills/<name>/SKILL.md`, `commands/*.md`, `.mcp.json` with an `mcpServers` wrapper), `marketplace.json` fields and relative `source`, commands named `/<plugin>:<command>`, project `.mcp.json` with `type: "http"` and its approval prompt. `claude plugin validate` (Claude Code 2.1.246) passes for both the plugin and the marketplace. | The `/plugin marketplace add Parallel-Platforms/hootlens` install itself (it needs the pushed repository), and the plugin's MCP entry check, which needs Claude Code 2.1.281 or later. |
| Cursor | Project `.cursor/mcp.json` with `mcpServers` and a `url`; `.cursor/rules/*.mdc` frontmatter (`description`, `globs`, `alwaysApply`) and that `AGENTS.md` is supported. | Whether a remote server with no `auth` object completes OAuth by dynamic registration inside Cursor. |
| VS Code | `.vscode/mcp.json` with a top-level `servers` object and `type: "http"`. | Automatic OAuth for remote servers: the page fetched did not describe it. |
| Codex | `[mcp_servers.<name>]` with `url`, project-scoped `.codex/config.toml` for trusted projects, `codex mcp login <name>`. | The `codex mcp add <name> --url` form used in the dashboard (the page showed only the stdio form), and OAuth for this server. |

None of this was exercised against a live Hoot Lens MCP deployment. The hosted endpoint at `mcp.hootlens.com` is not deployed or verified by this repository.

## Signing in

The CLI itself needs no sign-in: the project id is public. Only two optional features use a token, both through `HOOTLENS_PAT` (a personal access token with read scope from the dashboard): listing your projects in `init`, and the hosted check in `status`. It is sent only as a bearer header to the Hoot Lens API (`HOOTLENS_API` overrides the origin) and never printed or written. Reading the sign-in saved by `npx @hootlens/mcp login` is not implemented yet because that package's storage was not available to inspect; until it is, use `HOOTLENS_PAT` or pass `--project`.

## Publishing

`.github/workflows/publish-cli.yml` publishes `packages/cli` as `hootlens` when a tag `cli-v<version>` is pushed. It checks the tag against `package.json`, runs typecheck, lint and the CLI tests, builds, and runs `npm publish --access public --provenance` using npm trusted publishing (GitHub Actions OIDC, no token secret). The package owner must first create the package or reserve the name and register the repository and workflow as a trusted publisher on npmjs.com. Nothing has been published.
