Skip to content

fix biometric session scoping for non-TTY processes#675

Merged
theoephraim merged 7 commits into
mainfrom
fix/no-tty-session-scoping
May 1, 2026
Merged

fix biometric session scoping for non-TTY processes#675
theoephraim merged 7 commits into
mainfrom
fix/no-tty-session-scoping

Conversation

@theoephraim

Copy link
Copy Markdown
Member

Summary

  • When varlock is invoked from processes without a controlling TTY (e.g., Claude Code or other extensions in VSCode/Cursor), the daemon had no stable identity to cache the biometric session against, causing Touch ID prompts on every single decrypt call.
  • Fixes this by walking the process tree when no TTY is available and using the "grandchild of the app root" as the session scope key — narrow enough to prevent cross-extension piggyback while stable across all commands spawned by the same tool.
  • Also applies the same process-tree-walking logic to the WSL fallback path in the Node.js client.
  • Removes outdated patch-isolated/minor-isolated docs from AGENTS.md.

Algorithm

  1. Try TTY first (existing behavior, unchanged)
  2. If no TTY: build ancestry chain from peer PID up to app root (PPID=1)
  3. Use process at chain[count-3] (grandchild of app root) + start time as session key
  4. If tree is too shallow (<4 levels), return nil (no caching, fresh auth each time — safe default)

Example for Claude in Cursor:

[0] node/bun (varlock load)
[1] zsh (shell spawned by Claude)
[2] claude binary           ← scope target
[3] extension-host
[4] Cursor (PPID=1)

Test plan

  • Rebuild Swift binary and verify Touch ID only prompts once per Claude session in Cursor
  • Verify terminal-based usage still works (TTY path unchanged, format prefixed with tty:)
  • Verify shallow process trees (< 4 levels) gracefully degrade to fresh auth

When varlock is invoked from processes without a controlling TTY (e.g.,
Claude Code or other extensions in VSCode/Cursor), the daemon had no
stable identity to cache the biometric session against, causing Touch ID
prompts on every single decrypt call.

Fix by walking the process tree when no TTY is available and using the
"grandchild of the app root" as the session scope key. This picks a
stable ancestor that is narrow enough to prevent cross-extension
piggyback (e.g., the Claude binary PID) while being shared across all
commands spawned by that tool.

Algorithm:
1. Build ancestry chain from peer PID up to the app root (PPID=1)
2. Use the process at chain[count-3] (grandchild of app root) + start time
3. If tree is too shallow (<4 levels), return nil (no caching, safe default)

Also applies the same logic to the WSL fallback path in the Node.js client.
@theoephraim theoephraim force-pushed the fix/no-tty-session-scoping branch from 061b080 to e8db843 Compare April 30, 2026 21:03
@github-advanced-security

Copy link
Copy Markdown
Contributor

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

…back

The core bug: dev_t is Int32 on macOS, so NODEV is -1. Comparing
Int32(-1) != UInt32.max evaluates to true in Swift's BinaryInteger,
causing hasTty to always be true for no-TTY processes. The TTY code
path then fails at devname() and returns nil, never reaching the
ptree fallback. Fixed by using ttyDev > 0.

Also renames ttyId to sessionId throughout Swift/Rust/TS since it now
represents either a TTY or process-tree session identifier. Adds ptree
fallback to the Rust Linux daemon (mirrors the macOS Swift logic), and
includes start time in the WSL self-reported ptree key for PID-reuse
resistance. Adds unit tests for the Rust proc-stat parsing and ptree
walk.
- Add 30s timeout to sendMessage (5min for interactive biometric/picker)
- Add withRetry wrapper: auto-reconnect and retry on timeout/disconnect
- Add killDaemonProcess with SIGTERM→SIGKILL escalation, handles unkillable UE-state zombies
- Verify socket responsiveness in spawnDaemon instead of blindly trusting PID
- Reject all pending messages on socket close so callers don't hang
- Add forceCleanup to kill daemon + remove stale files before respawn
Comment thread packages/encryption-binary-rust/src/ipc.rs Dismissed
Comment thread packages/encryption-binary-rust/src/ipc.rs Dismissed
Comment thread packages/encryption-binary-rust/src/ipc.rs Dismissed
@pkg-pr-new

pkg-pr-new Bot commented May 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/varlock@675

commit: f8dcb6d

- Add 60s timeout to biometric semaphore.wait() in SessionManager
- Use _exit() instead of exit() to skip LA framework cleanup hangs
- Add BIOMETRIC_TIMEOUT_MS (90s) for decrypt/keychain-get operations
- Centralize daemon state file cleanup (including lock file)
- Clean stale PID files in checkDaemonBinaryStale when process is dead
- Log spawn errors instead of swallowing them silently
- Retry flock on fresh inode when lock held by unkillable process
@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

bumpy-frog

The changes in this PR will be included in the next version bump.

patch Patch releases

  • varlock 1.0.0 → 1.0.1

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@theoephraim theoephraim merged commit 7b7c94d into main May 1, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants