πŸ“„ File detail

utils/deepLink/banner.ts

🧩 .tsπŸ“ 124 linesπŸ’Ύ 4,694 bytesπŸ“ text
← Back to All Files

🎯 Use case

This file lives under β€œutils/”, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, …). On the API surface it exposes DeepLinkBannerInfo, buildDeepLinkBanner, and readLastFetchTime β€” mainly functions, hooks, or classes. Dependencies touch Node filesystem, Node OS/process metadata, Node path helpers, and review * carefully. It composes internal code from format and git (relative imports). What the file header says: Deep Link Origin Banner Builds the warning text shown when a session was opened by an external claude-cli:// deep link. Linux xdg-open and browsers with "always allow" set dispatch the link with no OS-level confirmation, so the application provides its own provenance signal β€” mir.

Generated from folder role, exports, dependency roots, and inline comments β€” not hand-reviewed for every path.

🧠 Inline summary

Deep Link Origin Banner Builds the warning text shown when a session was opened by an external claude-cli:// deep link. Linux xdg-open and browsers with "always allow" set dispatch the link with no OS-level confirmation, so the application provides its own provenance signal β€” mirroring claude.ai's security interstitial for external-source prefills. The user must press Enter to submit; this banner primes them to read the prompt (which may use homoglyphs or padding to hide instructions) and notice which directory β€” and therefore which CLAUDE.md β€” was loaded.

πŸ“€ Exports (heuristic)

  • DeepLinkBannerInfo
  • buildDeepLinkBanner
  • readLastFetchTime

πŸ“š External import roots

Package roots from from "…" (relative paths omitted).

  • fs
  • os
  • path
  • review * carefully

πŸ–₯️ Source preview

/**
 * Deep Link Origin Banner
 *
 * Builds the warning text shown when a session was opened by an external
 * claude-cli:// deep link. Linux xdg-open and browsers with "always allow"
 * set dispatch the link with no OS-level confirmation, so the application
 * provides its own provenance signal β€” mirroring claude.ai's security
 * interstitial for external-source prefills.
 *
 * The user must press Enter to submit; this banner primes them to read the
 * prompt (which may use homoglyphs or padding to hide instructions) and
 * notice which directory β€” and therefore which CLAUDE.md β€” was loaded.
 */

import { stat } from 'fs/promises'
import { homedir } from 'os'
import { join, sep } from 'path'
import { formatNumber, formatRelativeTimeAgo } from '../format.js'
import { getCommonDir } from '../git/gitFilesystem.js'
import { getGitDir } from '../git.js'

const STALE_FETCH_WARN_MS = 7 * 24 * 60 * 60 * 1000

/**
 * Above this length, a pre-filled prompt no longer fits on one screen
 * (~12-15 lines on an 80-col terminal). The banner switches from "review
 * carefully" to an explicit "scroll to review the entire prompt" so a
 * malicious tail buried past line 60 isn't silently off-screen.
 */
const LONG_PREFILL_THRESHOLD = 1000

export type DeepLinkBannerInfo = {
  /** Resolved working directory the session launched in. */
  cwd: string
  /** Length of the ?q= prompt pre-filled in the input box. Undefined = no prefill. */
  prefillLength?: number
  /** The ?repo= slug if the cwd was resolved from the githubRepoPaths MRU. */
  repo?: string
  /** Last-fetch timestamp for the repo (FETCH_HEAD mtime). Undefined = never fetched or not a git repo. */
  lastFetch?: Date
}

/**
 * Build the multi-line warning banner for a deep-link-originated session.
 *
 * Always shows the working directory so the user can see which CLAUDE.md
 * will load. When the link pre-filled a prompt, adds a second line prompting
 * the user to review it β€” the prompt itself is visible in the input box.
 *
 * When the cwd was resolved from a ?repo= slug, also shows the slug and the
 * clone's last-fetch age so the user knows which local clone was selected
 * and whether its CLAUDE.md may be stale relative to upstream.
 */
export function buildDeepLinkBanner(info: DeepLinkBannerInfo): string {
  const lines = [
    `This session was opened by an external deep link in ${tildify(info.cwd)}`,
  ]
  if (info.repo) {
    const age = info.lastFetch ? formatRelativeTimeAgo(info.lastFetch) : 'never'
    const stale =
      !info.lastFetch ||
      Date.now() - info.lastFetch.getTime() > STALE_FETCH_WARN_MS
    lines.push(
      `Resolved ${info.repo} from local clones Β· last fetched ${age}${stale ? ' β€” CLAUDE.md may be stale' : ''}`,
    )
  }
  if (info.prefillLength) {
    lines.push(
      info.prefillLength > LONG_PREFILL_THRESHOLD
        ? `The prompt below (${formatNumber(info.prefillLength)} chars) was supplied by the link β€” scroll to review the entire prompt before pressing Enter.`
        : 'The prompt below was supplied by the link β€” review carefully before pressing Enter.',
    )
  }
  return lines.join('\n')
}

/**
 * Read the mtime of .git/FETCH_HEAD, which git updates on every fetch or
 * pull. Returns undefined if the directory is not a git repo or has never
 * been fetched.
 *
 * FETCH_HEAD is per-worktree β€” fetching from the main worktree does not
 * touch a sibling worktree's FETCH_HEAD. When cwd is a worktree, we check
 * both and return whichever is newer so a recently-fetched main repo
 * doesn't read as "never fetched" just because the deep link landed in
 * a worktree.
 */
export async function readLastFetchTime(
  cwd: string,
): Promise<Date | undefined> {
  const gitDir = await getGitDir(cwd)
  if (!gitDir) return undefined
  const commonDir = await getCommonDir(gitDir)
  const [local, common] = await Promise.all([
    mtimeOrUndefined(join(gitDir, 'FETCH_HEAD')),
    commonDir
      ? mtimeOrUndefined(join(commonDir, 'FETCH_HEAD'))
      : Promise.resolve(undefined),
  ])
  if (local && common) return local > common ? local : common
  return local ?? common
}

async function mtimeOrUndefined(p: string): Promise<Date | undefined> {
  try {
    const { mtime } = await stat(p)
    return mtime
  } catch {
    return undefined
  }
}

/**
 * Shorten home-dir-prefixed paths to ~ notation for the banner.
 * Not using getDisplayPath() because cwd is the current working directory,
 * so the relative-path branch would collapse it to the empty string.
 */
function tildify(p: string): string {
  const home = homedir()
  if (p === home) return '~'
  if (p.startsWith(home + sep)) return '~' + p.slice(home.length)
  return p
}