DEV Community

Fernando Rodriguez
Fernando Rodriguez

Posted on • Originally published at frr.dev

When Security Asks Permission So Often You Stop Reading

Knock, knock. Who's there? Touch ID. Again.

Picture this: you're working in your terminal, pulling secrets from 1Password with op read. You need the Linear API key. Touch ID. The OpenRouter one. Touch ID. The Gitea token. Touch ID.

In half an hour it asked for my finger fourteen times.

You know what happens when a security tool interrupts you fourteen times in thirty minutes? By the fifth prompt, you're no longer reading what it's asking for. You place your finger reflexively. "Yes, whatever, let me work."

And that's exactly where security falls apart.

Auth fatigue: the problem nobody wants to acknowledge

This has a name in security circles: authorization fatigue. It's not a new concept. It's the same principle used in MFA fatigue attacks: bombard the user with authorization requests until they approve one out of sheer exhaustion.

In 2022, a 17-year-old breached Uber's internal systems exactly this way. He sent authentication push notifications to an employee over and over, in the middle of the night, until the person approved one just to get some sleep.

Obviously, 1Password asking for Touch ID isn't an attack. But the psychological effect is identical: it trains you to approve without thinking.

It's like those cookie banners that have been showing up on every website for years. You used to read them. Now you click "Accept All" without looking. Congratulations: a mechanism designed to protect your privacy has taught you to give it away faster.

Why 1Password kept asking for my finger

My setup: I use op read to pull secrets from 1Password in the terminal. Works great. The problem is I use Claude Code (an AI assistant in terminal), and every command it runs is a new process.

1Password has a 10-minute biometric session timeout that refreshes with each use. In theory, it shouldn't ask for fingerprint authentication that often. But Claude Code doesn't reuse processes: every time it needs a secret, it spawns a new shell, and 1Password treats it as a fresh session.

Result: Touch ID every time Claude needs a secret. Which is constantly.

The solution: a 40-line cache

The idea is simple: a wrapper that sits in front of op in your PATH. When you run op read, it checks if it already has the result cached and fresh. If yes, it returns it without touching 1Password. If not, it calls the real op, caches the result, and you're done.

For any other subcommand (op signin, op item list, etc.), it passes straight through to the real op without interference.

#!/bin/bash
# ~/.local/bin/op — Caching wrapper para 1Password CLI
# Solo cachea 'op read'. Todo lo demás pasa directo al op real.
# Cache TTL configurable con OP_CACHE_TTL (default: 3600s = 1h)

REAL_OP="/opt/homebrew/bin/op"
CACHE_DIR="${HOME}/.cache/op-cache"
CACHE_TTL="${OP_CACHE_TTL:-3600}"

# Solo cachear 'op read'
if [[ "$1" == "read" ]]; then
    mkdir -p "$CACHE_DIR" && chmod 700 "$CACHE_DIR"

    # Hash de todos los argumentos como clave de cache
    CACHE_KEY=$(printf '%s\0' "$@" | shasum -a 256 | cut -d' ' -f1)
    CACHE_FILE="${CACHE_DIR}/${CACHE_KEY}"

    # Cache hit: fichero existe y no ha expirado
    if [[ -f "$CACHE_FILE" ]]; then
        FILE_AGE=$(( $(date +%s) - $(stat -c %Y "$CACHE_FILE") ))
        if [[ $FILE_AGE -lt $CACHE_TTL ]]; then
            cat "$CACHE_FILE"
            exit 0
        fi
    fi

    # Cache miss o expirado: llamar al op real
    RESULT=$("$REAL_OP" "$@")
    EXIT_CODE=$?

    # Solo cachear si op tuvo éxito
    if [[ $EXIT_CODE -eq 0 ]]; then
        printf '%s' "$RESULT" > "$CACHE_FILE"
        chmod 600 "$CACHE_FILE"
    fi

    printf '%s' "$RESULT"
    exit $EXIT_CODE
else
    # Cualquier otro subcomando: pass-through directo
    exec "$REAL_OP" "$@"
fi
Enter fullscreen mode Exit fullscreen mode

Save it to ~/.local/bin/op, make it executable, and since ~/.local/bin comes before /opt/homebrew/bin in your PATH, your wrapper intercepts the calls.

The security decisions

Let's be honest about what we're doing: storing secrets in plain text on disk. That sounds terrible. But let's put it in context.

What it caches:

  • Only results from op read (individual secret lookups)
  • Everything else passes straight to the real op

Where it stores them:

  • ~/.cache/op-cache/ with 700 permissions (only your user)
  • Each cache file with 600 permissions (only read/write for you)

How long it lasts:

  • 1 hour by default, configurable with OP_CACHE_TTL

The filenames:

  • SHA-256 hashes of the arguments, they don't reveal which secret they contain

Is it more dangerous than a .env.local? No. It's exactly the same thing. Your .env.local files are also secrets in plain text on disk with restrictive permissions. And you have those in every project.

Is it more dangerous than what 1Password already does? The 1Password app keeps your vault decrypted in memory while it's unlocked. Our cache is more limited (only secrets you've read, not your entire vault) but less sophisticated (disk vs memory).

What worries us (and we don't know how to solve)

Here's the honest part. We're not security experts. We've made decisions that seem reasonable to us, but we might be missing something. Some concerns:

1. Should the cache clear when the screen locks?
Right now, if you lock your Mac and someone accesses the disk (theft, evil maid attack), the cached secrets are sitting there. Though if someone has access to your disk, you probably have bigger problems (FileVault should protect against this).

2. Are there race conditions?
If two processes run op read for the same secret simultaneously, both might try to write the cache at once. In practice this shouldn't cause serious problems (worst case is a partial read), but it's not clean.

3. Is the hash sufficient?
We use SHA-256 of the arguments as the filename. If someone has access to ~/.cache/op-cache/, they can't tell which secret is in each file, but they can read the contents of all of them. The 600 permissions should prevent this, but if there's a compromised process running as your user...

What could be improved

Some ideas we haven't implemented (yet):

  • Automatic cleanup of expired files (a cron job or launchd task that purges periodically)
  • Cache encryption with a key derived from the session
  • Notification when a secret is served from cache ("(cached)" to stderr)
  • Manual invalidation with op cache clear or similar

This is where you come in

Look, I'm writing this with the honesty of someone who knows they're not a security expert. We've made decisions that seem sensible to us. The threat model is clear: protect ourselves from authorization fatigue without opening obvious holes.

But "seems sensible to us" and "is secure" are two very different things.

If you know more about security than we do (which isn't hard) and you see an obvious flaw, an edge case we haven't considered, or just a better way to do this: tell me. Seriously. Comments are open and so is my email.

I'd rather be told "what you've built is a dangerous hack" than find out when it's too late.

The elephant in the room

Should 1Password solve this out of the box? Yes, probably. A configurable timeout per application, or a "work session" mode that kept authorization active for a defined period, would eliminate the need for this wrapper.

But until they do, the alternative is worse: keep placing your finger every 30 seconds until your brain disconnects and you start approving without looking.

Because that's what's paradoxical about excessive security: if the tool annoys you too much, you end up being less secure than if you didn't use it. At least without it you're aware that you're unprotected. With authorization fatigue, you think you're protected while approving anything with your eyes closed.

The best lock in the world is useless if the owner leaves the door open because they're tired of looking for the key.


Related: If you're curious why we centralized all secrets in 1Password, read 39 million secrets leaked on GitHub. And if you want to see what happens when you give an AI too many capabilities (spoiler: it sends 44 made-up emails), When your AI becomes your worst enemy is the horror story.

This article was originally written in Spanish and translated with the help of AI.

Top comments (0)