Guides

Provide entry points

Set one small global in your app so every annotation you capture carries your project context.

How it works

When your app loads, it sets a small window.__HAVI_DEV__ global with your project context. HAVI reads it when you capture an annotation and attaches it to the report.

It is optional and safe. If the global is missing, capture still works — the extra context is simply left out. Nothing runs on every request; the values are read straight from the page.

The fast way

Claude Code and Codex

Once HAVI is connected, run its setup-hooks prompt. Your agent detects your framework (Next.js, Vite, or Phoenix) and writes the setup — nothing to install.

prompt /setup-hooks

It is one of the HAVI slash-command prompts the connected server provides, alongside /review, /triage, and /resolve — pick it from the / menu.

What gets attached

project
Repository name
branch
Current git branch
commit
Short commit hash
worktree
Worktree name, when you work in one
port
Dev server port

Add anything else your app knows: environment name, app version, feature flags, or the active A/B variant. Those fields ride along on every report so the agent can act with full context.

Do it by hand

Any framework works — compute your context at build/boot and set window.__HAVI_DEV__ from it. Templates for the common ones:

Next.js
// next.config.js — compute git context once at server boot
const { execSync } = require("child_process");

const git = (cmd) => {
  try { return execSync(`git ${cmd}`, { encoding: "utf-8" }).trim(); }
  catch { return ""; }
};

const worktree =
  git("rev-parse --git-common-dir") !== git("rev-parse --git-dir")
    ? (process.cwd().split("/").pop() || "")
    : "";

const haviDev = {
  project: git("remote get-url origin").replace(/.*\//, "").replace(/\.git$/, ""),
  worktree,
  branch: git("rev-parse --abbrev-ref HEAD"),
  commit: git("rev-parse --short HEAD"),
  port: process.env.PORT || "3000",
  // add your own fields here, e.g.:
  env: process.env.NODE_ENV,
};

module.exports = { env: { NEXT_PUBLIC_HAVI_DEV: JSON.stringify(haviDev) } };

// In app/layout.tsx (App Router), inside <head>:
//   <script dangerouslySetInnerHTML={{ __html:
//     `window.__HAVI_DEV__ = ${process.env.NEXT_PUBLIC_HAVI_DEV || "{}"};` }} />
// (Pages Router: put the same <script> in pages/_document.tsx's <Head>.)
// Restart your dev server after switching branches — values are captured at boot.
Vite
// vite.config.js — inject git context at config load
import { execSync } from "child_process";
import { defineConfig } from "vite";

const git = (cmd) => {
  try { return execSync(`git ${cmd}`, { encoding: "utf-8" }).trim(); }
  catch { return ""; }
};

const worktree =
  git("rev-parse --git-common-dir") !== git("rev-parse --git-dir")
    ? (process.cwd().split("/").pop() || "")
    : "";

const haviDev = {
  project: git("remote get-url origin").replace(/.*\//, "").replace(/\.git$/, ""),
  worktree,
  branch: git("rev-parse --abbrev-ref HEAD"),
  commit: git("rev-parse --short HEAD"),
  port: Number(process.env.PORT) || 5173,
};

export default defineConfig({
  // double-encode: define does raw text substitution, so the value must be a string literal
  define: { "import.meta.env.VITE_HAVI_DEV": JSON.stringify(JSON.stringify(haviDev)) },
});

// In your entry (src/main.js):
//   try { window.__HAVI_DEV__ = JSON.parse(import.meta.env.VITE_HAVI_DEV || "{}"); } catch {}
// Restart your dev server after switching branches — values are captured at boot.
Phoenix
# lib/my_app_web/havi_dev.ex — capture git context once per boot
defmodule MyAppWeb.HaviDev do
  @moduledoc false

  def script do
    json =
      case :persistent_term.get({__MODULE__, :json}, nil) do
        nil ->
          value = Jason.encode!(context())
          :persistent_term.put({__MODULE__, :json}, value)
          value

        value ->
          value
      end

    Phoenix.HTML.raw("window.__HAVI_DEV__ = #{json};")
  end

  defp context do
    if Code.ensure_loaded?(Mix) and Mix.env() == :dev do
      %{
        project:
          git(~w[remote get-url origin])
          |> String.replace(~r|.*/|, "")
          |> String.replace(~r|\.git$|, ""),
        worktree: worktree(),
        branch: git(~w[rev-parse --abbrev-ref HEAD]),
        commit: git(~w[rev-parse --short HEAD]),
        port: System.get_env("PORT") || "4000"
      }
    else
      %{}
    end
  end

  defp worktree do
    if git(~w[rev-parse --git-common-dir]) != git(~w[rev-parse --git-dir]) do
      File.cwd!() |> Path.basename()
    else
      ""
    end
  end

  defp git(args) do
    case System.cmd("git", args, stderr_to_stdout: true) do
      {out, 0} -> String.trim(out)
      _ -> ""
    end
  rescue
    _ -> ""
  end
end

# In root.html.heex, inside <head>:
#   <script><%= MyAppWeb.HaviDev.script() %></script>
# Restart your dev server after switching branches — values are captured at boot.

Verify

Restart your dev server, then open your app and check window.__HAVI_DEV__ in the browser console. You should see your context as an object.

console window.__HAVI_DEV__

Values are captured at boot, so restart your dev server after switching branches. From here, your next captures arrive with project context already filled in.