Contributing
Contributing to NR AI Coding Observability: Preflight
This guide covers everything you need to get productive in this repo: environment setup, project architecture, code conventions, testing, and how to verify your changes end-to-end.
What Is This Project?
Preflight provides observability for AI coding assistants. When developers use tools like Claude Code, Cursor, Windsurf, or Copilot, this project captures what’s happening — tool calls, token usage, costs, efficiency patterns — and sends it all to New Relic.
MCP Server (this repo) — Hooks into Claude Code via the Model Context Protocol. Captures every tool call, computes metrics like efficiency scores and anti-pattern detection, and exposes MCP tools that Claude Code can query directly.
The MCP server uses a common transport layer (src/shared/) for event buffering, metric aggregation, and HTTP delivery to New Relic’s APIs.
Development Setup
Prerequisites
- Node.js v22 or later (v24 recommended; use
nvm installto get the version from.nvmrc) - npm 11.11 or later (Node 24 ships it; on Node 22,
npm install -g npm@latest). Older npm strips thelibcfields frompackage-lock.json, sonpm installrewrites 90 unrelated lines.npmwarns at install time when yours is too old. shellcheck(brew install shellcheck), for the pre-commit hook’s lint of tracked scripts under.claude/skills/jq, whichnpm testruns to check the release workflow’s version filters (recent macOS and GitHub’s Ubuntu runners include it; otherwisebrew install jqorapt install jq)- A New Relic account with a license key and account ID (for cloud-path testing)
First-time setup
nvm install # Install the right Node version (v24, from .nvmrc)nvm use # Activate itnpm ci # Install dependencies from the lockfilenpm run build # Build the server and the dashboard bundle, chmod +x the CLI binariesnpm link # Register preflight on PATH (required for Claude Code hooks)npm test # Jest suite — server, CLI, metricsnpm run test:web # Vitest suite — dashboard UIpreflight setup # Interactive wizard that inits NR keys, Claude Code hooks, dashboardsTo pull the latest changes and rebuild later:
preflight updateCommands
| Command | What it does |
|---|---|
npm run build |
Build the server (tsc --build), chmod the CLI binaries, then typecheck and build src/web |
npm run build:clean |
Remove build output |
npm test |
Run the full Jest suite (maxWorkers: 1) — everything except src/web |
npm run test:web |
Run the Vitest suite — the src/web dashboard UI |
npm run test:e2e |
Run the Playwright suite in e2e/ (rebuilds first via pretest:e2e) |
npm run test:e2e:update |
Rewrite the Playwright screenshot baselines for the OS you are on |
npm run test:e2e:update:linux |
Rewrite the -linux baselines in the Playwright image CI uses (needs Docker) |
npm run test:integration |
Run src/multi-instance.integration.test.ts |
npx tsc -p tsconfig.web.json |
Typecheck src/web, tests included (npm run build checks source only) |
npx tsc -p tsconfig.tools.json |
Typecheck scripts/, e2e/ and the root *.config.ts files, which no other config reaches |
npm run lint |
ESLint over src/, scripts/, e2e/ and the root *.config.ts files |
npm run format |
Prettier write |
npm run format:check |
Prettier check (no writes) |
npm run deploy:dashboard |
Deploy the default NR dashboard |
npm run deploy:dashboard:all |
Deploy every pre-built dashboard |
npm run deploy:dashboard:update |
Sync every pre-built dashboard in place (preserves GUID/URL) |
npm run deploy:dashboard:teardown |
Delete every pre-built dashboard (matches by name; missing = skipped) |
npm run deploy:alerts |
Deploy the alert policy + conditions to NR |
npm run deploy:alerts:update |
Sync conditions on the existing alert policy in place |
npm run deploy:alerts:teardown |
Delete the alert policy and all its conditions |
npm run backfill:sessions |
Backfill local session JSON files from NR event history |
npm run dev |
Start local dashboard server (--local); assumes dist/ already built |
npm run dev:all |
Build then start local dashboard (npm run build && npm run dev) |
npm run dev:full |
Build backend, then start backend + Vite dev server together (open http://localhost:5173) |
npm run start:local |
Alias for npm run dev |
To run a single test file:
npx jest -- src/metrics/cost-tracker.test.tsnpx jest -- src/shared/harvest/harvest-scheduler.test.tsTo build directly without the chmod step:
npx tsc -b .Git worktrees
A new worktree starts with no node_modules/ and no .husky/_ — both are generated rather than
tracked, so neither comes across with the checkout. Install in the worktree before working in it:
git worktree add ../preflight-my-feature -b fix/my-featurecd ../preflight-my-featurenpm ci # Installs deps and regenerates .husky/_ via the prepare scriptWorking with shared code
Do not edit src/shared/ directly — it is a vendored snapshot. If you find a bug there, please open an issue at https://github.com/newrelic-experimental/preflight/issues.
Project Structure
This is a flat single-package repo. Source lives directly under src/. There is no packages/ directory and no npm workspaces.
preflight/ src/ shared/ # Transport, events, pricing, harvest scheduler (vendored snapshot) hooks/ # Hook collector + pre/post event pairing metrics/ # metric analyzer classes (session, cost, anti-patterns, efficiency, …) tools/ # MCP tool handlers proxy/ # HTTP proxy + upstream transports storage/ # JSON session and weekly summary persistence security/ # Audit trail + SSRF helpers tracing/ # OTel span lifecycle transport/ # NR ingest manager + log ingest platforms/ # one adapter per supported platform (see docs/ADAPTERS.md) + generic MCP fallback digest/ # Slack digest formatter and sender install/ # preflight install / setup CLI alerts/ # Alert TS types (JSON files live in alerts/ at repo root) deploy/ # `deploy-dashboards` and `deploy-alerts` subcommands alerts/ # Alert policy + condition JSON definitions (bundled into dist/data/alerts/) dashboards/ # Pre-built NR dashboard JSON files (bundled into dist/data/dashboards/) scripts/ # backfill-sessions.ts, check-bundle-size.tsFor a complete annotated tree, see CLAUDE.md.
Shared transport layer (src/shared/)
The foundation layer is vendored in src/shared/. Provides:
- Event creation —
createAiRequest(),createAiResponse(), serialization to NR format - Transport — HTTP clients for New Relic’s Events, Metric, and Logs APIs, plus an OTLP/HTTP exporter
- Harvest scheduler — Periodic flush of buffered events (5s) and metrics (60s) with bounded retry buffers
- Token utilities — Extract token counts from Anthropic/Gemini API responses
- Pricing — Calculate USD cost from token counts using model-specific pricing tables
- Logger —
createLogger('name')writes structured JSON to stderr
MCP server subsystems
-
Hooks (
src/hooks/) — Claude Code invokes a hook script on every tool use. The collector writes events to a local JSONL buffer. The event processor drains the buffer, pairs pre/post events, and emitsToolCallRecordobjects. -
Metrics (
src/metrics/) — metric analyzers that each receive tool call records and maintain running state. Session tracking, cost tracking + forecasting, task detection, anti-pattern detection, efficiency scoring, trend analysis, collaboration profiling, and more. -
Tools (
src/tools/) — MCP tool handlers that query the metric trackers and return results. Registered viaregisterTools()insrc/tools/session-stats.ts. -
Proxy (
src/proxy/) — HTTP proxy layer that forwards requests to upstream MCP servers while recording latency and tool call metrics. -
Storage (
src/storage/) — Local file persistence for session summaries and weekly aggregations under~/.newrelic-preflight/. -
Security (
src/security/) — Audit trail that classifies tool calls and flags sensitive file access or destructive commands; SSRF validation for outbound URLs. -
Tracing (
src/tracing/) — OTel span management. Emits a session root span, intermediate task spans fromTaskDetectorboundaries, and a leaf span perToolCallRecord.
Key Concepts
See ARCHITECTURE.md for a full data-flow diagram and component reference. The concepts below cover the most important building blocks.
ToolCallRecord
The central data type. Every tool call captured by the hooks becomes a ToolCallRecord with fields like toolName, durationMs, success, filePath, command, exitCode, etc. This record flows through all metric trackers.
HarvestScheduler
Events and metrics are buffered in memory and flushed to New Relic on a timer. Events flush every 5 seconds, metrics every 60 seconds. Failed batches are re-queued with a bounded retry buffer. The scheduler handles graceful shutdown by awaiting a final flush.
Metric Trackers
All trackers follow the same pattern:
tracker.recordToolCall(record); // feed data intracker.getMetrics(); // read state outtracker.reset(sessionId); // clear for new sessionEach tracker has a corresponding test file with factory helpers.
MCP (Model Context Protocol)
The server communicates with Claude Code over stdio using JSON-RPC. It registers tools that Claude Code can discover and invoke. The @modelcontextprotocol/sdk package handles the protocol; our code registers tool handlers and implements the business logic.
Code Conventions
TypeScript
- ESM modules with
.jsimport extensions (required for NodeNext resolution) - Strict mode enabled
readonlyon all interface fieldsinterfacefor API contracts,typefor unions and local aliases- Never use
as any— useas unknown as Tfor forced coercions - Never add
eslint-disablecomments — fix the underlying issue
File organization
- One module per file, co-located tests:
foo.ts+foo.test.ts - Files:
kebab-casenaming - Classes:
PascalCase, functions:camelCase, constants:SCREAMING_SNAKE_CASE
Import order
- Node.js builtins (
node:fs,node:path) - External packages (
@modelcontextprotocol/sdk,zod) - (blank line)
- Shared imports (
../shared/index.js) - Local imports (
./types.js)
Logging
Every module creates a scoped logger:
import { createLogger } from '../shared/index.js';const logger = createLogger('my-module');Logger writes to stderr as JSON. Never write to stdout — it’s reserved for the MCP stdio transport.
Error handling
- Failed network sends re-queue batches for retry (bounded buffer)
- Graceful degradation: if a tracker is unavailable, tools return sensible defaults
try/catcharound file I/O operations with logger warnings- Clock skew protection:
Math.max(0, ...)on computed durations
Testing
Tests live next to the code they test (foo.test.ts alongside foo.ts).
The three suites
npm test is not the whole story — there are three runners, split by what they can execute:
| Suite | Command | Covers | Runner |
|---|---|---|---|
| Server | npm test |
src/**/*.test.ts outside src/web, plus test/**/*.test.ts |
Jest, node env |
| Web | npm run test:web |
src/web/**/*.test.{ts,tsx} |
Vitest, jsdom env |
| Browser | npm run test:e2e |
e2e/*.spec.ts |
Playwright, real Chromium |
Two files sit outside all three: jest.config.ts also ignores src/shared/index.test.ts, which
asserts an invariant that does not hold for vendored source, and
src/multi-instance.integration.test.ts, which spawns real processes and runs on demand via
npm run test:integration. Both carry a comment saying why.
npm run test:e2e installs its browser if needed, rebuilds, then starts two dashboards itself —
one over an empty store, one over a seeded one — each against its own temp directory, which
also stands in for HOME. So it needs no running server, and it reads none of your credentials,
your real ~/.newrelic-preflight, or your Claude Code and Copilot transcripts. See
TEST_PATTERNS.md for adding a test.
.claude/skills/verify-preflight/ is the only project skill tracked in git. It launches an isolated
Preflight from your checkout and drives it through the collector, the dashboard, and the stdio MCP
tools (.claude/skills/verify-preflight/SKILL.md). Any other skill in .claude/skills/, including
symlinks to skills kept elsewhere, stays ignored. To track a new project skill, add a
!.claude/skills/<name>/ line to .gitignore. Keep personal skills untracked through
.git/info/exclude. Tracked skill Markdown is covered by npm run format:check, and tracked shell
scripts by npm run lint:shell (shellcheck).
Screenshot assertions compare against per-platform baselines in e2e/*-snapshots/. Two are
committed for each screenshot: -darwin, recorded on a Mac, and -linux, recorded in the
mcr.microsoft.com/playwright image whose tag matches @playwright/test in package.json —
the image CI’s e2e job runs in, so CI checks the Linux one. When a deliberate UI change makes
them stale, re-record both and commit the result:
- macOS:
npm run test:e2e:update. - Linux:
npm run test:e2e:update:linux, from any OS with Docker running. It records in the pinned image aslinux/amd64, so on Apple Silicon it runs emulated and takes a few minutes. Your checkout has to be under a directory Docker shares with its VM (your home directory, by default).
A Linux desktop run compares against the committed -linux baseline and may fail on font
differences alone; the container is the reference. There is no Windows baseline: a first run
on Windows writes one and fails, and later runs compare against it. Keep it uncommitted, since
nothing in CI checks it. Adding a new screenshot means committing its -darwin and -linux
files together. CI’s e2e job fails on a missing -linux one; it runs Linux only, so nothing
catches a missing or stale -darwin one but the next macOS run.
Writing tests
let stderrSpy: ReturnType<typeof jest.spyOn>;
beforeEach(() => { stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);});
afterEach(() => { stderrSpy.mockRestore();});
function makeRecord(overrides?: Partial<ToolCallRecord>): ToolCallRecord { return { id: 'rec-001', toolName: 'Read', /* sensible defaults */ ...overrides };}- Suppress logger output by mocking
process.stderr.write - Use
make*factory functions with optionalPartial<T>overrides - Use
jest.useFakeTimers()for anything time-dependent - Create temp directories for storage tests, clean up in
afterEach
See TEST_PATTERNS.md for the full testing guide.
Before opening a PR
-
npm run buildsucceeds -
npm testpasses -
npm run test:webpasses (if you touchedsrc/web) -
npm run test:e2epasses (if you changed the dashboard UI; CI runs it too, against the-linuxbaselines — see The three suites for re-recording them) -
npx tsc -p tsconfig.web.jsonpasses (if you touchedsrc/web;npm run buildchecks source only) -
npx tsc -p tsconfig.tools.jsonpasses (if you touchedscripts/,e2e/or a root*.config.ts) -
npm run lintpasses -
npm run format:checkpasses (thepre-commithook runs this too) -
npm run lint:shellpasses (if you touched a script under.claude/skills/; thepre-commithook runs this too) -
npm run check:test-ownershippasses (if you added, moved or renamed a test file, or touchedjest.config.ts,vitest.config.tsor atest:*script) -
package-lock.jsonis unchanged bynpm install --package-lock-onlyunless you changed dependencies - You added a changeset if the PR changes anything that ships, and left the version,
CHANGELOG.md, and other PRs’ pending changesets alone (see Changesets; CI’schangesetjob checks all three) - You’ve reviewed your own diff
Contributing Changes
External contributors — fork workflow
- Fork the repo on GitHub
- Clone your fork:
git clone https://github.com/<your-username>/preflight - Create a branch:
git checkout -b fix/my-fix - Make your changes, then run the checks in Before opening a PR
- Push to your fork and open a PR against
main
Commit messages
Type: Short descriptionTypes: Fix, Feat, Refactor, Chore, Test, Docs
If you used an AI coding assistant: add a Co-Authored-By trailer with the model name, e.g.:
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Branches
Use descriptive branch names: yourname/short-description or fix/issue-description.
Changesets
A PR never bumps Preflight’s version or edits CHANGELOG.md; only the release PR does (see
Releasing). Instead, the PR adds a changeset: a Markdown file in
.changeset/ that names the semver bump the change needs and holds its
CHANGELOG entry.
---'@newrelic/preflight': minor---
**Scripts and fleet tooling could read `preflight doctor` results only by parsing itshuman-readable output.** `preflight doctor --json` prints the checks as a JSON array on stdout,with the same exit codes as the human-readable mode.Create one with npx changeset, which asks for the bump and the summary, or in one line with
npx changeset --patch @newrelic/preflight -m "..." (or --minor, --major). You can also
write the file by hand. If you do, give it a name that says what it’s about, such as
.changeset/doctor-json-output.md. Commit it along with the change it describes. A PR
that makes several changes worth their own entries can add several changesets.
Changesets don’t conflict the way version bumps did. Every PR used to bump the version in five files and add its entry at the top of the CHANGELOG, so each merge put every other open PR in conflict. Each changeset is its own file, and the release PR is the only thing that bumps the version.
Which PRs need one. CI’s changeset job fails when a PR changes a file that ships and adds
no changeset. The files that ship are listed under changedFilePatterns in
.changeset/config.json: everything in src/ except tests, the plugin and Kiro power, the data
directories, package.json, package-lock.json, server.json, and smithery.yaml, plus the
build config that shapes dist/ (tsconfig.json, the two tsconfig.web*.json files,
vite.config.ts, and postcss.config.js). The lockfile counts because the dashboard bundle in
dist/web is built from the versions it pins.
A PR that touches only docs, tests, CI, or scripts needs no changeset. If a PR changes a shipped
file in a way users won’t notice, such as bumping a devDependency that only the tests or the
build use, add an empty changeset with npx changeset --empty. That satisfies the check
without adding a CHANGELOG entry. The dashboard’s own libraries (React, TanStack Query, and the
rest of what Vite bundles) are devDependencies too, but bumping one changes what users get, so
give it a patch changeset.
Release only refuses to run past a pending changeset that bumps the version (see Releasing). An empty changeset that merges after the release PR ships in that release without being mentioned, which is why it’s only for changes users won’t notice.
Picking the bump. The release takes the highest bump among its pending changesets.
patch: fixes, and changes users won’t notice.minor: anything new a user can use, such as a config field, a CLI command or flag, an MCP tool, an event type or attribute, or a dashboard view.major: anything that breaks a working setup, such as removing or renaming a config field, CLI flag, MCP tool, or event attribute that dashboards and alerts query, or raising the minimum Node.js version.
Writing the entry. Write it the way the existing CHANGELOG entries read: for someone who
uses Preflight, about what changed for them and why, not how the code changed. A fix says what
was wrong and what happens now. When an entry runs long, lead with one bolded sentence. The
summary goes into the CHANGELOG exactly as written, and Prettier checks .changeset/ files
like any others, so run npm run format before committing.
Changing a changeset that’s already on main. Edit its text in place, raise its bump, or
rename the file without changing it. CI fails a PR that deletes a pending changeset, empties
it, or lowers its bump, since any of those quietly drops a change from the next release. A
maintainer can merge over the check for a PR that reverts the change a changeset describes,
or when a reviewer agrees a bump should be lower. CI also fails a PR whose own changeset it
can’t read, so write each bump as '@newrelic/preflight': patch (or minor, or major). If
an unreadable one reaches main anyway, every PR fails and Release refuses to run until a PR
fixes or removes it.
Releasing
- While
mainhas changesets that need a release,release-pr.ymlkeeps a Chore: Release x.y.z PR open. It runsnpm run version-packages, which bumpspackage.json, adds the CHANGELOG entry, and deletes the changesets it used. Thenscripts/release-files.tscopies the new version intopackage-lock.json,server.json, and both plugin manifests, and dates the entry. Each push tomainrebuilds the PR, so to change an entry, edit its changeset onmaininstead of pushing to the PR’s branch. The entry’s date is the day the PR was last rebuilt. If that’s stale, start the Release PR workflow onmainfrom the Actions tab (Run workflow) before merging. Don’t use Re-run on an older run: a re-run builds from that run’s commit, so the PR would lose any changeset that landed since. - Review the release PR, then close and reopen it right before you merge, and wait for CI.
GitHub doesn’t let a PR opened by the built-in
GITHUB_TOKENstart workflows, so CI doesn’t run on it by itself. A reopen runs CI on the PR merged into the currentmain, so it also tests whatever landed after the PR was built, such as a new test that reads the version files. Then merge it. Every push tomaingives the PR a new head commit, which drops its checks and its approval. When that happens, reopen it again and approve only once CI passes:mainrequires the approval but not CI, so the approval is the only gate. Before merging, also check the Actions tab: no Release PR run should be queued or in progress, and the latest one shouldn’t have failed, since until a run pushes, the PR still shows the old commit’s green checks. If a push lands while the PR is closed, the workflow opens a new release PR instead; leave the old one closed, and close and reopen the new one before merging it. - Right after the release PR merges, run the Release workflow
(
release.yml) onmainfrom the Actions tab. Run from any other branch, it skips its job and publishes nothing. It checks that every version file agrees (npm run check:release-files), tagsvX.Y.Z, creates the GitHub release, and publishes to npm, the Homebrew tap, and the MCP Registry.
Release tags and publishes main as it stands when it runs. A changeset that merges after the
release PR goes into the next release’s CHANGELOG entry, so if Release ran after it, x.y.z
would ship that change without mentioning it. Release refuses to run while main has such a
changeset. When that happens, merge the next release PR, which release-pr.yml opens as soon as
the changeset lands, and run Release then. That release also ships everything in x.y.z, which
keeps its CHANGELOG entry but never reaches npm under its own number.
If Release fails after tagging, for example at the npm publish, use Re-run failed jobs on
that run, which uses the commit it started from. If only the Homebrew tap step failed, the run
still finishes green (that step is allowed to fail, and shows a warning), so use Re-run all
jobs instead; the steps already done skip themselves. Don’t start a new run: by then main
may have moved past the tagged commit, and Release refuses to run when the existing tag points
at a different commit than the one it checked out, so it can’t publish that commit under the
tag’s version. The GitHub release’s notes are the version’s CHANGELOG entry.
If the Release PR workflow can’t run, run npm run version-packages on a branch named
changeset-release/main off main, commit the result, and open the PR yourself. CI’s
changeset job skips only that branch, since a release PR bumps the version.
Security
This codebase sends telemetry to New Relic and can spawn child processes and proxy network requests:
- Redact before you log or send. Any string that might contain secrets must pass through
redact()(agent) orredactSensitive()(MCP server) before it reaches a logger or NR event. - Validate external strings at the boundary.
accountIdis validated as 1–12 decimal digits at config load. Tool names are truncated and stripped of control characters. - Subprocess commands need absolute paths.
StdioUpstreamrejects relative command names and strips dangerous env vars (LD_PRELOAD, etc.) before spawning. - HTTP upstream URLs are SSRF-checked.
HttpUpstreamrejects non-http:/https:schemes and RFC-1918/loopback hosts. - High security mode is absolute. When
highSecurity=true,recordContentis alwaysfalse. Never bypass this.
See SECURITY.md for the full guidelines and code review checklist.
Platform Support
The MCP server registers an adapter per AI coding platform and auto-detects the active one at startup — each adapter’s isSupported() checks its own platform-specific env vars (most do not use a shared NEW_RELIC_AI_PLATFORM variable; only the Kiro and Copilot adapters read it).
Platform capabilities vary: some platforms expose a real hook mechanism that captures every built-in tool call, others only support MCP as a client (so Preflight can only see calls routed to its own MCP tools, not the platform’s built-in file/shell tools).
See docs/ADAPTERS.md for the full per-platform reference — integration mechanism, detection env vars, tool-name mapping, known gaps, and setup steps.
Deploying to New Relic
Dashboards
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 preflight deploy-dashboards --allDeploys all seven pre-built dashboards. Use --print to output JSON for manual import via the NR UI.
For a self-reflection dashboard pre-filtered to your identity:
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \ preflight deploy-dashboards ai-coding-assistant-personal.json --developer <your-name>To replace existing dashboards in place (preserves GUID and URL), add --update:
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \ preflight deploy-dashboards --all --updateTo remove all deployed dashboards:
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \ preflight deploy-dashboards --all --teardownAlert conditions
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 preflight deploy-alertsDeploys the “AI Coding Assistant Alerts” policy with five NRQL conditions. Use --dry-run to preview without hitting the API.
To sync conditions in place on an existing policy (preserves policy ID; matches conditions by name to update, creates new ones, deletes removed ones):
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 preflight deploy-alerts --updateFor per-developer alerts scoped to one identity:
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \ preflight deploy-alerts --developer <your-name>This creates a separate policy AI Coding — Personal — <name> from alerts/conditions-personal/, with developer = '<name>' injected into every NRQL query. Use --teardown --developer <name> to remove just the personal policy.
To remove all deployed alert conditions:
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 preflight deploy-alerts --teardownTerraform (IaC alternative)
A Terraform module in terraform/ deploys all 7 dashboards and all 10 alert conditions as an alternative to the scripts above. See ADVANCED.md — Terraform Deployment for full usage, variables.
Verifying Your Setup
After making changes, run through these checkpoints to confirm end-to-end behavior. Work through one path (cloud or local) at a time.
Prerequisites
| Item | Check |
|---|---|
| Node.js v22+ | node --version → v22.x.x or later |
| npm v10+ | npm --version → 10.x.x |
| Claude Code (latest) | Opens and launches |
| Cloud path only: New Relic account with a license key + user API key | See README |
1. Build and link
git clone https://github.com/newrelic-experimental/preflightcd preflightnvm usenpm cinpm run buildnpm linkCheckpoint: preflight --help prints the command list. If you see command not found, the npm link step didn’t work.
2. Run the setup wizard
preflight setupFor cloud path, choose cloud and supply your license key, account ID, and (optionally) your user API key. For local path, choose local.
Checkpoint: cat ~/.newrelic-preflight/config.json shows the values you entered.
3. Restart Claude Code
Quit and reopen Claude Code. The MCP server starts automatically.
Checkpoint — MCP connection: In a new Claude Code session, ask:
Call
nr_observe_healthand show me the result.
Expected:
{ "status": "ok", "version": "1.0.x", "developer": "your-name", "session_id": "some-uuid", "uptime_seconds": 3}If you see tool not found or MCP server unavailable, the server didn’t start. Check Claude Code’s MCP output panel (View → Output → MCP) for errors, then re-run preflight install and restart.
WSL users: Pass --windows-cc or --linux-cc to target the right Claude Code installation. Run preflight setup if unsure — the wizard will ask.
Checkpoint — local dashboard (local path only):
curl -s http://127.0.0.1:7777/api/health# Expected: {"ok":true,"uptime":<number>}4. Generate activity and verify
In Claude Code:
Read the file README.md and summarize it in one sentence.
Then:
Call
nr_observe_get_session_statsand show me the result.
Expected: tool_calls > 0, non-zero session_duration_ms.
Cloud path: Open your NR account → Dashboards → search “AI Coding”. Within 1–2 minutes:
SELECT count(*) FROM AiToolCall WHERE developer = 'your-name' SINCE 5 minutes agoExpected: a non-zero count.
Local path: Open http://127.0.0.1:7777 and confirm the Today tab shows tool call count > 0.
5. Smoke test anti-pattern detection
Read README.md. Read it again. Read it a third time. Now call
nr_observe_get_anti_patterns.
Expected: a re_reading entry for README.md with read_count: 3.
Deploy dashboards and alerts (cloud path)
# DashboardsNEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \ preflight deploy-dashboards --all
# Alerts (optional)NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \ preflight deploy-alertsRe-deploying? Use
--updateto sync in place and avoid creating duplicates.
Checkpoint: Open NR → Dashboards → search AI Coding. You should see 7 dashboards listed.
Teardown / reset
To remove all hooks and start fresh:
preflight uninstall --yes # --yes skips the confirmation promptrm -rf ~/.newrelic-preflight
# Cloud: remove dashboards and alerts from NRNEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \ preflight deploy-dashboards --all --teardownNEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \ preflight deploy-alerts --teardownThen restart Claude Code.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Anything unexpected | Config, hooks, connectivity, or daemon issue | Run preflight doctor — prints six checks with actionable fixes |
preflight: command not found |
npm link not run |
Run npm link in the repo root |
nr_observe_health returns tool-not-found |
MCP server not started | Restart Claude Code; check MCP output panel |
| No data in NR after 5 minutes | Wrong license key or account ID | Re-run preflight setup with correct credentials |
| Dashboard at 7777 unreachable | Port in use or mode is not local | Check lsof -i:7777; confirm config.json has "mode": "local" |
| Hook not firing | preflight not on PATH when Claude Code launched |
Run npm link, then restart Claude Code |
Invalid account ID in wizard |
Entered a non-numeric value | Account IDs are digits only (e.g. 3456789) |
Where to Get Help
- CLAUDE.md — Full technical reference: architecture, conventions, every pattern in detail.
- SECURITY.md — Security practices, invariants, and code review checklist. Read before any PR touching config loading, network requests, subprocess execution, or telemetry fields.
- TEST_PATTERNS.md — Testing conventions, factory patterns, mock strategies.
- COMMANDS_TABLE.md — All MCP tools with parameters and return schemas.
- METRICS_TABLE.md — Every NR event and metric, field definitions, delivery mechanism.
- The code itself — Best examples of our patterns:
src/metrics/(tracker pattern),src/shared/harvest/(scheduler/buffer pattern).