Skip to content

TOTP Authentication

Daniel Ellison edited this page Mar 2, 2026 · 1 revision

TOTP Authentication

Optional two-factor authentication that gates Claude startup. When configured, Kai prompts for a 6-digit code from an authenticator app before starting the Claude process. Without a valid code, Claude never starts - no shell, no file access, no attack surface.

TOTP is entirely opt-in. If not configured, the bot behaves exactly as it does today.

Why session-gate instead of per-workspace

Claude has full shell access regardless of which workspace is active. An attacker with Telegram access doesn't need to switch workspaces - they can run cat ~/.ssh/id_rsa from any directory. The real security boundary is whether Claude is running at all. Once you authenticate, workspace switching is free.

Prerequisites

Install the TOTP optional dependency:

pip install -e '.[totp]'

This adds pyotp (TOTP implementation) and qrcode (QR code generation). Both are pure Python with no platform-specific concerns.

If the dependency is not installed, the TOTP gate is fully disabled and the bot runs normally.

Setup

Setup creates a root-owned secret file that the bot user cannot read directly. This is critical - the inner Claude process runs as the bot user and has full filesystem access to anything that user owns. A root-owned file ensures the secret is unreadable regardless of what Claude is instructed to do.

Step 1: Run setup

sudo python -m kai totp setup

This will:

  1. Generate a random base32 secret
  2. Write it to /etc/kai/totp.secret (root-owned, mode 0600)
  3. Create an attempts tracking file at /etc/kai/totp.attempts (root-owned, mode 0600)
  4. Display a QR code to scan with your authenticator app
  5. Print the raw secret for manual entry
  6. Print the required sudoers rules
  7. Ask for a 6-digit confirmation code

Important: Service account users. If your bot runs as a dedicated service account (e.g., kai) that doesn't have sudo access, you'll need to run setup from a user account that does. Log in as yourself, cd to the project directory, activate the virtualenv, and run the setup command there. The runtime bot process doesn't need sudo for general use - the sudoers rules (step 2) give it targeted access to just the two TOTP files.

Step 2: Configure sudoers

The setup command prints the exact rules to add. Create the sudoers file using visudo:

sudo visudo -f /etc/sudoers.d/kai

Add the rules printed during setup. On macOS they look like:

kai ALL=(root) NOPASSWD: /bin/cat /etc/kai/totp.secret
kai ALL=(root) NOPASSWD: /bin/cat /etc/kai/totp.attempts
kai ALL=(root) NOPASSWD: /usr/bin/tee /etc/kai/totp.attempts

On Linux, /bin/cat becomes /usr/bin/cat. The setup command detects the platform and prints the correct paths.

These rules allow the bot process to:

  • Read the TOTP secret (to verify codes)
  • Read and write the attempts file (to track failed attempts and lockouts)

Nothing else. Claude cannot reset the failure counter or read the secret outside of these narrow sudo calls.

Complete this step before restarting the bot. Without the sudoers rules, the bot can't read the secret file and will behave as if TOTP is not configured.

Step 3: Restart the bot

Restart Kai. On the next message, you'll be prompted for an authenticator code.

Daily use

When your TOTP session expires (default: 30 minutes of inactivity), the next message triggers a challenge:

You:  Hey, what's the weather?
Kai:  Session expired. Enter code from authenticator.
You:  482917
Kai:  Authenticated.

Your code message is automatically deleted from the chat after verification, so it doesn't linger in the conversation history.

After authenticating, send your actual message - the code itself is consumed by the gate and not forwarded to Claude.

What requires re-authentication

  • First message after bot startup (auth state is ephemeral)
  • After TOTP_SESSION_MINUTES of inactivity (default 30)

What does NOT require re-authentication

  • Workspace switches (/workspace)
  • Session resets (/new)
  • Model changes (/model)
  • Bot commands that don't invoke Claude (/stats, /help, /workspaces, etc.)

The auth timestamp lives in memory (context.user_data), not in the database. A bot restart always requires re-authentication - this is intentional.

Configuration

All settings are optional and go in .env. Defaults are shown:

# Minutes of inactivity before TOTP re-authentication is required
TOTP_SESSION_MINUTES=30

# Seconds the challenge window stays open after prompting
TOTP_CHALLENGE_SECONDS=120

# Number of failed attempts before temporary lockout
TOTP_LOCKOUT_ATTEMPTS=3

# Duration of lockout in minutes after the threshold is exceeded
TOTP_LOCKOUT_MINUTES=15

Rate limiting

After 3 consecutive failed attempts (configurable), further verification is blocked for 15 minutes, even with a valid code. The failure counter resets on a successful verification.

Rate limiting state is persisted to disk (in the root-owned attempts file) so lockouts survive bot restarts. Claude cannot reset this counter.

CLI commands

All commands use python -m kai totp <subcommand>. Setup and reset require root; status does not.

Command Requires root Description
setup Yes Generate secret, display QR code, confirm with a test code
status No Check whether TOTP is configured
reset Yes Remove secret and attempts files, disabling TOTP

Checking status

python -m kai totp status
# "TOTP is configured." or "TOTP is not configured."

Resetting TOTP

If you lose access to your authenticator, get locked out, or want to disable TOTP:

sudo python -m kai totp reset

This deletes /etc/kai/totp.secret and /etc/kai/totp.attempts. The bot will start without TOTP on the next message. To reconfigure, run setup again.

Security model

What TOTP protects against

  • Stolen Telegram session - an attacker who gains access to your Telegram account cannot invoke Claude without your authenticator
  • Physical access to unlocked phone - same as above

What TOTP does not protect against

  • Root access to the host - root can read the secret file directly
  • Bot code modification - an attacker with write access to the source code could disable the gate

TOTP is one layer in a defense-in-depth approach. It works alongside Telegram user ID filtering (ALLOWED_USER_IDS) and the local-only deployment model.

Secret storage details

Path Owner Mode Purpose
/etc/kai/totp.secret root:root 0600 Base32 TOTP shared secret
/etc/kai/totp.attempts root:root 0600 JSON rate-limiting state (failure count, lockout timestamp)

Both files are invisible to the bot user and to any subprocess it spawns (including inner Claude). Access is only through the sudoers-authorized commands.

Cross-platform notes

  • /etc/kai/ works on both macOS and Linux
  • cat path differs: /bin/cat (macOS) vs /usr/bin/cat (Linux). The setup command detects the platform automatically.
  • tee is at /usr/bin/tee on both platforms
  • pyotp and qrcode are pure Python - no platform-specific builds
  • Sudoers syntax is identical on both platforms

Clone this wiki locally