Fingerprint Biometric Gate
July 27, 2026 ยท View on GitHub
Use your phone's fingerprint sensor to approve sensitive operations before they execute. A Claude Code hook (a script Claude Code runs automatically before a tool call) calls termux-fingerprint, a command from Termux (the Android terminal app this guide uses), checks the result, and blocks the action if authentication fails.
What It Does
The fingerprint gate adds a biometric confirmation step before destructive or high-impact operations:
- Pushing to public repositories
- Creating pull requests
- Force-pushing, resetting, or deleting branches
- Any other operation you define as sensitive
If the fingerprint check fails or is dismissed, the operation is blocked. Nothing runs until the device owner physically confirms it.
Requirements
-
Termux:API package. Install in Termux:
pkg install termux-api -
Termux:API companion app. Install from F-Droid (an open-source Android app store; search "Termux:API" there). The package provides the CLI commands; the companion app provides the Android permissions bridge. Both are required. Without the companion app,
termux-fingerprintfails silently. -
Source matching. Termux and Termux:API must come from the same source (both F-Droid or both GitHub releases). Mixing sources causes signature mismatches and silent failures.
-
A registered fingerprint on the device (Settings > Security > Fingerprint).
Verify it works before wiring it into hooks:
termux-fingerprintTouch the sensor. A working setup returns JSON output containing
"auth_result": "AUTH_RESULT_SUCCESS".
How It Works
termux-fingerprint prompts for biometric authentication via the Android system dialog. It returns JSON:
{
"auth_result": "AUTH_RESULT_SUCCESS"
}
On failure (wrong finger, dismissed, timeout):
{
"auth_result": "AUTH_RESULT_FAILURE",
"errors": ["ERROR_TIMEOUT"]
}
A helper function parses auth_result and returns exit code 0 (approved) or 1 (denied). Your hook script sources this function and calls it before allowing the sensitive operation to proceed.
Setup
The gate script is also available as examples/fingerprint-gate.sh in this repository.
Step 1: Create the gate function
Create a file that any hook can source. Put it wherever makes sense for your setup. ~/.claude/hooks/ is a natural choice if you keep your hooks there.
~/.claude/hooks/fingerprint-gate.sh
Dependency: This uses
jqto parse JSON. Install it withpkg install jqif you don't have it.
#!/usr/bin/env bash
# Fingerprint biometric gate. Source this file, then call require_fingerprint.
require_fingerprint() {
if ! command -v termux-fingerprint >/dev/null 2>&1; then
echo "termux-fingerprint not found. Install: pkg install termux-api" >&2
echo "Also install the Termux:API companion app from F-Droid." >&2
return 1
fi
local result
result=$(termux-fingerprint)
local auth_result
auth_result=$(echo "$result" | jq -r '.auth_result // ""')
if [ "$auth_result" = "AUTH_RESULT_SUCCESS" ]; then
echo "Fingerprint verified." >&2
return 0
else
echo "Fingerprint denied. Operation blocked." >&2
return 1
fi
}
Step 2: Create a hook script
Create a hook that sources the gate and checks for sensitive operations. This example blocks git push to public repos and destructive git commands:
~/.claude/hooks/pre-tool-use-fingerprint-gate.sh
#!/usr/bin/env bash
# Block sensitive git operations without fingerprint approval
# Source the gate function
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/fingerprint-gate.sh"
# Read the JSON payload Claude Code sends on stdin
INPUT=$(cat)
# Extract tool name and command from JSON
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# Only gate Bash tool calls
if [ "$TOOL_NAME" != "Bash" ]; then
exit 0
fi
# Check for sensitive git operations
case "$COMMAND" in
*"git push"*|*"git push --force"*|*"git reset --hard"*|*"git branch -D"*|*"gh pr create"*)
echo "Sensitive operation detected: requesting fingerprint approval..." >&2
if require_fingerprint; then
exit 0 # Approved
else
echo '{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Fingerprint authentication failed. Operation blocked."}}'
exit 0
fi
;;
*)
exit 0 # Not a sensitive operation, allow it
;;
esac
Claude Code sends the hook a JSON payload on stdin (tool_name, tool_input, and more) and reads the allow/block decision back from the JSON the hook prints on stdout, not from its exit code. That is why the script exits 0 on both approval and denial: on denial it prints a hookSpecificOutput object with "permissionDecision": "deny", and that object is what blocks the operation. For the full payload and output schema, see the Claude Code hooks documentation.
Make it executable:
chmod +x ~/.claude/hooks/fingerprint-gate.sh
chmod +x ~/.claude/hooks/pre-tool-use-fingerprint-gate.sh
Bypass vectors to be aware of: The
case "$COMMAND"pattern matching above operates on the raw command string and is illustrative rather than airtight. Predictable ways for a malicious or careless prompt to slip past it:
- Variable interpolation:
cmd="git push"; $cmd: the raw command contains the variable assignment, not the literal substring"git push", so the case pattern doesn't fire.- Subshell expansion:
$(echo git push): the raw command contains the subshell expression, not the resulting command.- Whitespace tricks:
git$'\t'pushorgit push(double space): glob*"git push"*requires the exact single-space substring.- Aliased forms:
gp(shell alias forgit push), or any user-defined script that calls git push.If your threat model includes a prompt-injection attacker who knows how this hook is wired, build the check on a parsed view of the command. Two safer shapes:
- Tokenize
$COMMANDwithset --and inspect$1/$2against an allowlist.- Configure git itself to require a credential helper that triggers the fingerprint prompt. This moves the gate inside git, where command-line tricks don't help.
The example here is good for "I want a fingerprint nag before destructive operations during normal use." It is not a security boundary against an adversary who can run arbitrary shell from inside a Claude Code session.
Step 3: Register the hook in Claude Code settings
Add the hook to your Claude Code settings so it runs before tool use:
~/.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-tool-use-fingerprint-gate.sh"
}
]
}
]
}
}
Each matcher entry holds a hooks array. Every item in it needs "type": "command" and a command pointing at your script. The matcher field controls which tool triggers the hook. Setting it to "Bash" means the hook only fires for shell commands. You can also use "*" to fire on every tool call, or name a specific tool like "Write".
Customizing the Gate
Gate different operations
Modify the case statement in the hook script to match whatever commands you consider sensitive. Some ideas:
*"rm -rf"*) # Recursive deletion
*"git rebase"*) # History rewriting
*"npm publish"*) # Package publishing
*"gh release"*) # GitHub releases
Gate only repos on a given host
Check the remote URL before requiring fingerprint. Note that the host alone does not tell you whether a repo is public: private repositories are served from the same github.com host as public ones, and there is no reliable way to tell them apart from the remote URL. This example simply gates any repo hosted on GitHub:
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "")
case "$REMOTE_URL" in
*"github.com"*)
# Hosted on GitHub (public or private). Require fingerprint.
require_fingerprint || exit 1
;;
*)
# No GitHub remote. Allow.
;;
esac
Multiple hooks
You can register multiple hooks for the same event. Claude Code runs them in order; if any hook blocks, the operation stops:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-tool-use-fingerprint-gate.sh"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-tool-use-file-safety.sh"
}
]
}
]
}
}
Troubleshooting
termux-fingerprint: command not found
pkg install termux-api
Then install the Termux:API companion app from F-Droid.
Fingerprint prompt never appears The Termux:API companion app is missing or was installed from a different source than Termux. Uninstall both, reinstall both from F-Droid.
jq: command not found
pkg install jq
Hook doesn't fire
Check that settings.json is valid JSON and the hook path is correct. Test the hook directly:
echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' \
| bash ~/.claude/hooks/pre-tool-use-fingerprint-gate.sh
Fingerprint works in terminal but not in hooks
Hooks run in a subprocess. The termux-fingerprint binary needs to be on PATH in that context. Try using the full path: /data/data/com.termux/files/usr/bin/termux-fingerprint.
Security Notes
- The fingerprint check runs locally on your device. No biometric data leaves the phone.
termux-fingerprintuses Android's built-in BiometricPrompt API, the standard system biometric prompt.- The gate is only as strong as your hook configuration. If a command can bypass the hook (e.g., by not matching the
casepattern), it won't be gated. - This is a speed bump for autonomous AI operations, not a security boundary. A determined attacker with device access has other vectors. Its purpose is ensuring the device owner approves consequential actions before they happen.
Last updated: 2026-07-01.