Skip to content

Wire hardware-based CLI fingerprint into login flow#528

Merged
jahooma merged 3 commits intomainfrom
jahooma/cli-fingerprint
Apr 21, 2026
Merged

Wire hardware-based CLI fingerprint into login flow#528
jahooma merged 3 commits intomainfrom
jahooma/cli-fingerprint

Conversation

@jahooma
Copy link
Copy Markdown
Contributor

@jahooma jahooma commented Apr 21, 2026

Summary

The hardware-based fingerprint in cli/src/utils/fingerprint.ts (machine-id + CPU + MAC + hostname → SHA-256) existed but was never imported — every login path used Math.random(), so every session.fingerprint_id row was unique and multi-account clustering signals (maxFpShare/maxSigShare) were stuck at 1.

This wires getFingerprintId() into both the TUI login modal and the plain-text login, caches the promise once per process, and pre-fetches during initializeApp so it's resolved by the time the user hits Enter. The resolved id lives in login-store so polling reads from the same place that set it. Added node-machine-id as an explicit CLI dep (previously only transitive via nx). Applies to both codebuff and freebuff since both build from the same cli/ source.

Also deletes two dead duplicates of generateFingerprintId and the fully-unused cli/src/components/login-modal-utils.ts.

Test plan

  • Verify a new session row's fingerprint_id now starts with enhanced-…
  • Re-login on the same machine yields the same fingerprint_id
  • Plain-text login (codebuff login) also produces the enhanced- prefix

🤖 Generated with Claude Code

The enhanced hardware fingerprint (machine-id + CPU + MAC + hostname, SHA-256)
in cli/src/utils/fingerprint.ts existed but was never imported, so every login
path used Math.random() and every session.fingerprint_id was unique — killing
any multi-account clustering signal (maxFpShare/maxSigShare stuck at 1).

Cache the promise once per process and pull the id into login-store so both
TUI login and plain-text login ship the same hardware hash. Pre-fetch during
initializeApp so it's resolved by the time the user hits Enter. Dropped two
dead duplicates of generateFingerprintId and the unused login-modal-utils.ts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 21, 2026

Greptile Summary

This PR wires the existing hardware-based getFingerprintId() (machine-id + CPU + MAC + hostname → SHA-256) into the two login paths (TUI modal and plain-text login) and removes three Math.random()-based duplicates that were silently overriding it. A process-lifetime promise cache is added so all auth steps within a session share the same fingerprint, and initializeApp pre-warms it in the background so the result is typically ready before the user presses Enter.

Key changes:

  • getFingerprintId() added to fingerprint.ts with a module-level promise cache; calculateFingerprint() remains as the private implementation.
  • login-store gains fingerprintId state so the polling hook reads from the same source that the login mutation set it.
  • node-machine-id promoted to an explicit CLI dependency.
  • Dead files login-modal-utils.ts and the duplicate generateFingerprintId in login/utils.ts deleted.
  • One P2 suggestion: the fingerprintId! non-null assertion in use-login-polling.ts is safe today due to ordering guarantees, but adding fingerprintId to the effect guard would make the contract explicit.

Confidence Score: 5/5

Safe to merge; the single P2 is a defensive style improvement that doesn't affect correctness today.

No P0/P1 issues found. The ordering invariant that makes fingerprintId! safe is correctly documented and holds in the current code. The caching strategy is sound, the legacy fallback path remains intact, dead code is cleanly removed, and the explicit dep addition is appropriate.

cli/src/hooks/use-login-polling.ts — the implicit null guard (P2 suggestion only)

Important Files Changed

Filename Overview
cli/src/utils/fingerprint.ts Adds getFingerprintId() with process-lifetime promise caching; removes redundant try/catch wrapper in getMachineId(); calculateFingerprint() remains exported as underlying impl
cli/src/state/login-store.ts Adds `fingerprintId: string
cli/src/hooks/use-login-polling.ts Loosens fingerprintId param type to `string
cli/src/components/login-modal.tsx Replaces local useState(() => generateFingerprintId()) with an async await getFingerprintId() call inside fetchLoginUrlAndOpenBrowser; stores result in login-store before mutating
cli/src/init/init-app.ts Fires void getFingerprintId() during app init to warm the hardware fingerprint before the user hits Enter on the login prompt
cli/src/login/plain-login.ts Swaps generateFingerprintId() for await getFingerprintId() so plain-text login also uses the hardware fingerprint
cli/src/login/utils.ts Deletes the Math.random()-based generateFingerprintId() duplicate; remaining helpers are unaffected
cli/src/components/login-modal-utils.ts Deleted — contained another Math.random() duplicate of generateFingerprintId and an unreferenced parseLogoLines; removal is clean
cli/package.json Adds node-machine-id ^1.1.12 as an explicit direct dependency; previously only transitive

Sequence Diagram

sequenceDiagram
    participant App as initializeApp
    participant FP as fingerprint.ts
    participant Modal as LoginModal
    participant Store as login-store
    participant Poll as useLoginPolling
    participant API as Login API

    App->>FP: void getFingerprintId() [pre-warm]
    Note over FP: starts calculateFingerprint() promise,<br/>caches it in cachedFingerprintPromise

    Note over Modal: user presses Enter
    Modal->>FP: await getFingerprintId()
    FP-->>Modal: "enhanced-<hash>" (or legacy fallback)
    Modal->>Store: setFingerprintId(id)
    Modal->>API: fetchLoginUrlMutation.mutate(id)
    API-->>Modal: { loginUrl, fingerprintHash, expiresAt }
    Modal->>Store: setFingerprintHash / setExpiresAt / setLoginUrl

    Poll->>Store: reads fingerprintId, fingerprintHash
    Note over Poll: guard: all 4 values non-null?
    Poll->>API: pollLoginStatus({ fingerprintId, fingerprintHash, ... })
    API-->>Poll: { status: "success", user }
    Poll->>Modal: onSuccess(user)
Loading

Reviews (1): Last reviewed commit: "Wire hardware-based CLI fingerprint into..." | Re-trigger Greptile

Comment thread cli/src/hooks/use-login-polling.ts Outdated
jahooma and others added 2 commits April 21, 2026 16:17
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
With the new hardware-based fingerprint, legit users on shared dev machines,
Docker images with baked-in /etc/machine-id, CI runners, and corporate golden
images can all produce the same fingerprint. Hard-blocking in that case would
lock out coworkers behind whichever user logged in first.

Keep the "Fingerprint ownership conflict" warn log as input for async abuse
review, but always proceed to createCliSession so the signal never gates
login on its own.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jahooma jahooma merged commit ad8bd4f into main Apr 21, 2026
19 checks passed
@jahooma jahooma deleted the jahooma/cli-fingerprint branch April 21, 2026 23:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant