Contact
READING PROGRESS:0%
PRIMITIVE

Why clicking Always Allow on a macOS keychain prompt does nothing, and the hidden partition list blocking your tools.

Intricate geometric astrological and navigational travel chart from Qanoon-e-Islam (1832)

Ja'far Sharif, Diagrams for Travel (1832). Astrological pathways where every marked turn promises safe passage but circles back to the origin.

SOURCE: Ja'far Sharif, Diagrams for Travel (1832), Public Domain ReviewARCHIVE PLATE ↗

You write a Python automation script that stores an API token in the macOS system keychain. You fire off a shell command to read it back. A modal window pops up in the center of your screen:

“security wants to access key ‘api-token’ in your keychain.”

You click Always Allow.

You run the script a second time. The dialog returns immediately. You click Always Allow again. On the third run, it pops up twice in a row.

The button isn’t broken. It’s answering a question nobody asked.

# What you think happened:
# "security CLI now has permission to read 'api-token'."

# What macOS actually did:
# Updated an Access Control List (ACL) that was skipped entirely.

The Two-Gate Security Model

Since macOS Sierra (10.12), every item stored in the Apple Keychain carries two separate layers of authorization:

  1. Gate 1: The Partition List. A low-level attribute that dictates which classes of binaries may read the item without prompting (Apple-signed binaries, the security CLI tool, or apps matching a specific Developer Team ID).
  2. Gate 2: The Application ACL. The list of individual application paths allowed to read the secret.

When you click Always Allow in the native GUI prompt, macOS modifies Gate 2. It adds /usr/bin/security to the application ACL.

Here is the trap: macOS evaluates Gate 1 before it ever touches Gate 2.

If the partition list on the keychain item does not explicitly include apple-tool: or the reader’s team identity, macOS drops authorization immediately. It never reads the application ACL. The “Always Allow” button successfully edited gate two, but your process was turned away at gate one.

The Mixed-Writer Problem

Why does this only bite developers and automation engineers? Because normal macOS applications read and write their own data using the same bundle ID.

The bug appears when you have a mixed-writer stack:

  • Your setup script writes the secret using Python’s keyring library or the native Security.framework API.
  • Your runtime CLI or background daemon reads the secret using /usr/bin/security find-generic-password -w -s "my-service".

When Python writes to the keychain, it stamps the item with an empty or application-restricted partition list. When /usr/bin/security attempts to read it back, macOS sees a foreign tool attempting to access an item whose partition list does not include apple-tool:.

It triggers an interactive password prompt. You click Always Allow. The system writes /usr/bin/security to the ACL, but leaves the partition list untouched. The next read fails the exact same check.

The One Command That Fixes It

You cannot fix a broken partition list from the macOS Keychain Access GUI. Apple provides no toggle for it.

You have to repair the partition list directly using the command line:

security set-generic-password-partition-list \
  -a "$ACCOUNT_NAME" \
  -s "$SERVICE_NAME" \
  -S "apple-tool:,apple:" \
  -k "$KEYCHAIN_PASSWORD" \
  login.keychain-db

The magic flag is -S "apple-tool:,apple:".

This stamps the keychain item so that both Apple-signed system frameworks and Apple command-line tools like /usr/bin/security are recognized in Gate 1.

Once Gate 1 allows the check to proceed, macOS consults the Application ACL you modified with Always Allow. The prompt vanishes for good.

The Write-Path Rule

A one-time bash script to repair existing keys is not enough.

Every time a background task rotates a session token or rewrites a secret using Python keyring.set_password(), the underlying API resets the partition list back to default. The next automated run will hang on an interactive modal.

If you write secrets via Python, enforce the partition list immediately following the write:

import subprocess
import keyring

def store_automation_secret(service: str, account: str, secret: str, keychain_pw: str):
    # 1. Write secret via standard library
    keyring.set_password(service, account, secret)

    # 2. Immediately stamp Gate 1 partition access
    cmd = [
        "security", "set-generic-password-partition-list",
        "-a", account,
        "-s", service,
        "-S", "apple-tool:,apple:",
        "-k", keychain_pw,
        "login.keychain-db"
    ]
    subprocess.run(cmd, check=True, capture_output=True)

Bonus Footnote: The 128-Character Trap

If you try to bypass keyring and write long secrets directly via /usr/bin/security add-generic-password -w, beware:

Honest Caveats

  • Keychain Password Requirement: Modifying the partition list via security set-generic-password-partition-list requires passing your login keychain password. In an interactive dev workstation, that’s fine. In an unattended CI/CD runner, storing your keychain password in an environment variable introduces its own attack vector.
  • Apple Tool Boundary: Granting apple-tool: allows any Apple-signed command-line binary to read that specific credential without a prompt. If an untrusted script runs under your user account, it can invoke /usr/bin/security to fetch the secret. Only stamp apple-tool: on service tokens meant for local automation, never on root master credentials.

Use the framework to write the full string, stamp the partition list on the write path, and stop clicking the button that cannot help you.

STAY IN THE LOOP