-
Notifications
You must be signed in to change notification settings - Fork 8
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.
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.
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 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.
sudo python -m kai totp setupThis will:
- Generate a random base32 secret
- Write it to
/etc/kai/totp.secret(root-owned, mode 0600) - Create an attempts tracking file at
/etc/kai/totp.attempts(root-owned, mode 0600) - Display a QR code to scan with your authenticator app
- Print the raw secret for manual entry
- Print the required sudoers rules
- 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.
The setup command prints the exact rules to add. Create the sudoers file using visudo:
sudo visudo -f /etc/sudoers.d/kaiAdd 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.
Restart Kai. On the next message, you'll be prompted for an authenticator code.
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.
- First message after bot startup (auth state is ephemeral)
- After
TOTP_SESSION_MINUTESof inactivity (default 30)
- 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.
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=15After 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.
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 |
python -m kai totp status
# "TOTP is configured." or "TOTP is not configured."If you lose access to your authenticator, get locked out, or want to disable TOTP:
sudo python -m kai totp resetThis 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.
- 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
- 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.
| 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.
-
/etc/kai/works on both macOS and Linux -
catpath differs:/bin/cat(macOS) vs/usr/bin/cat(Linux). The setup command detects the platform automatically. -
teeis at/usr/bin/teeon both platforms -
pyotpandqrcodeare pure Python - no platform-specific builds - Sudoers syntax is identical on both platforms