source dump of claude code
at main 123 lines 4.7 kB view raw
1/** 2 * Deep Link Origin Banner 3 * 4 * Builds the warning text shown when a session was opened by an external 5 * claude-cli:// deep link. Linux xdg-open and browsers with "always allow" 6 * set dispatch the link with no OS-level confirmation, so the application 7 * provides its own provenance signal — mirroring claude.ai's security 8 * interstitial for external-source prefills. 9 * 10 * The user must press Enter to submit; this banner primes them to read the 11 * prompt (which may use homoglyphs or padding to hide instructions) and 12 * notice which directory — and therefore which CLAUDE.md — was loaded. 13 */ 14 15import { stat } from 'fs/promises' 16import { homedir } from 'os' 17import { join, sep } from 'path' 18import { formatNumber, formatRelativeTimeAgo } from '../format.js' 19import { getCommonDir } from '../git/gitFilesystem.js' 20import { getGitDir } from '../git.js' 21 22const STALE_FETCH_WARN_MS = 7 * 24 * 60 * 60 * 1000 23 24/** 25 * Above this length, a pre-filled prompt no longer fits on one screen 26 * (~12-15 lines on an 80-col terminal). The banner switches from "review 27 * carefully" to an explicit "scroll to review the entire prompt" so a 28 * malicious tail buried past line 60 isn't silently off-screen. 29 */ 30const LONG_PREFILL_THRESHOLD = 1000 31 32export type DeepLinkBannerInfo = { 33 /** Resolved working directory the session launched in. */ 34 cwd: string 35 /** Length of the ?q= prompt pre-filled in the input box. Undefined = no prefill. */ 36 prefillLength?: number 37 /** The ?repo= slug if the cwd was resolved from the githubRepoPaths MRU. */ 38 repo?: string 39 /** Last-fetch timestamp for the repo (FETCH_HEAD mtime). Undefined = never fetched or not a git repo. */ 40 lastFetch?: Date 41} 42 43/** 44 * Build the multi-line warning banner for a deep-link-originated session. 45 * 46 * Always shows the working directory so the user can see which CLAUDE.md 47 * will load. When the link pre-filled a prompt, adds a second line prompting 48 * the user to review it — the prompt itself is visible in the input box. 49 * 50 * When the cwd was resolved from a ?repo= slug, also shows the slug and the 51 * clone's last-fetch age so the user knows which local clone was selected 52 * and whether its CLAUDE.md may be stale relative to upstream. 53 */ 54export function buildDeepLinkBanner(info: DeepLinkBannerInfo): string { 55 const lines = [ 56 `This session was opened by an external deep link in ${tildify(info.cwd)}`, 57 ] 58 if (info.repo) { 59 const age = info.lastFetch ? formatRelativeTimeAgo(info.lastFetch) : 'never' 60 const stale = 61 !info.lastFetch || 62 Date.now() - info.lastFetch.getTime() > STALE_FETCH_WARN_MS 63 lines.push( 64 `Resolved ${info.repo} from local clones · last fetched ${age}${stale ? ' — CLAUDE.md may be stale' : ''}`, 65 ) 66 } 67 if (info.prefillLength) { 68 lines.push( 69 info.prefillLength > LONG_PREFILL_THRESHOLD 70 ? `The prompt below (${formatNumber(info.prefillLength)} chars) was supplied by the link — scroll to review the entire prompt before pressing Enter.` 71 : 'The prompt below was supplied by the link — review carefully before pressing Enter.', 72 ) 73 } 74 return lines.join('\n') 75} 76 77/** 78 * Read the mtime of .git/FETCH_HEAD, which git updates on every fetch or 79 * pull. Returns undefined if the directory is not a git repo or has never 80 * been fetched. 81 * 82 * FETCH_HEAD is per-worktree — fetching from the main worktree does not 83 * touch a sibling worktree's FETCH_HEAD. When cwd is a worktree, we check 84 * both and return whichever is newer so a recently-fetched main repo 85 * doesn't read as "never fetched" just because the deep link landed in 86 * a worktree. 87 */ 88export async function readLastFetchTime( 89 cwd: string, 90): Promise<Date | undefined> { 91 const gitDir = await getGitDir(cwd) 92 if (!gitDir) return undefined 93 const commonDir = await getCommonDir(gitDir) 94 const [local, common] = await Promise.all([ 95 mtimeOrUndefined(join(gitDir, 'FETCH_HEAD')), 96 commonDir 97 ? mtimeOrUndefined(join(commonDir, 'FETCH_HEAD')) 98 : Promise.resolve(undefined), 99 ]) 100 if (local && common) return local > common ? local : common 101 return local ?? common 102} 103 104async function mtimeOrUndefined(p: string): Promise<Date | undefined> { 105 try { 106 const { mtime } = await stat(p) 107 return mtime 108 } catch { 109 return undefined 110 } 111} 112 113/** 114 * Shorten home-dir-prefixed paths to ~ notation for the banner. 115 * Not using getDisplayPath() because cwd is the current working directory, 116 * so the relative-path branch would collapse it to the empty string. 117 */ 118function tildify(p: string): string { 119 const home = homedir() 120 if (p === home) return '~' 121 if (p.startsWith(home + sep)) return '~' + p.slice(home.length) 122 return p 123}