# Fingerprint Biometric Gate

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

1. **Termux:API package.** Install in Termux:
   ```sh
   pkg install termux-api
   ```

2. **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-fingerprint` fails silently.

3. **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.

4. **A registered fingerprint** on the device (Settings > Security > Fingerprint).

> **Verify it works** before wiring it into hooks:
> ```sh
> termux-fingerprint
> ```
> Touch 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:

```json
{
  "auth_result": "AUTH_RESULT_SUCCESS"
}
```

On failure (wrong finger, dismissed, timeout):

```json
{
  "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`](../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 `jq` to parse JSON. Install it with `pkg install jq` if you don't have it.

```sh
#!/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`**

```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](https://docs.claude.com/en/docs/claude-code/hooks).

Make it executable:

```sh
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'push` or `git  push` (double space): glob `*"git push"*` requires the exact single-space substring.
> - **Aliased forms:** `gp` (shell alias for `git 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 `$COMMAND` with `set --` and inspect `$1`/`$2` against 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`**

```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:

```sh
*"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:

```sh
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:

```json
{
  "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`**
```sh
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`**
```sh
pkg install jq
```

**Hook doesn't fire**
Check that `settings.json` is valid JSON and the hook path is correct. Test the hook directly:
```sh
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-fingerprint` uses 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 `case` pattern), 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.*
