The -w option of security add-generic-password can be used in two ways. You can put the secret straight after -w, which makes it part of the command's arguments, where other processes can see it. Or you can put -w at the very end with no value, and security asks for it at a prompt. The man page marks the second way as the recommended one: "Put at end of command to be prompted (recommended)".
That second way is what I used on 10 September 2026, when a local tool needed to store an access token for a third-party service in the Keychain. The point was to keep the token out of the command's arguments, where other processes could read it through ps. The token was a JWT, 842 characters long. What could be read back out of the Keychain was 128 characters, and nothing said a word about it.
A prompt that asks for the value twice
The value is fed to that prompt through printf and a pipe. Here is the store command from my notes, where the account and service names were already written as placeholders. I have translated those placeholders, and the variable, into English:
printf '%s\n%s\n' "$VALUE" "$VALUE" | security add-generic-password -a account -s service -wThe value appears twice because the prompt asks for confirmation. Send it only once and the command fails with "passwords don't match". So the prompt is careful enough to reject two entries that differ. That care does not extend to length.
Exit status 0, a clean read-back, a different value
The store command exited with status 0. Reading the value back with security find-generic-password -w succeeded too. The two signals people usually rely on to call a write good were both green, and the value that came back was still not the value that went in.
My notes keep the result of sending values at four different lengths:
sent 100 -> returned 100 intact
sent 128 -> returned 128 intact
sent 129 -> returned 128 TRUNCATED
sent 200 -> returned 128 TRUNCATEDThe limit is exactly 128 characters. A 128-character value still comes back whole, while a 129-character value already comes back as 128. The truncation is completely silent. For an 842-character token, that means 842 minus 128, so 714 characters gone, and only about 15 percent of the token left, since 128 divided by 842 is roughly 0.15.
The man page's description of -w mentions no length limit at all. My notes also record only the behavior, not where the number 128 comes from, and I would rather leave the cause open than invent one.
The first complaint comes from the server
The symptom does not show up where the damage happens. A truncated JWT still looks like a token. A JWT is three base64url segments joined by dots, and a cut-off piece of such a string is made of the same kind of characters. My guess is that if the cut happens to land in the right spot, a truncated token could even still have three dot-separated parts.
It only gets rejected at the far end, by the server, as 401 Unauthorized. An answer like that points at the request that was sent rather than at the place the token was stored, so all the attention went to the shape of the header, the scheme name, and whether Origin was complete. The storage was what was broken.
Print the length, not the value
What cracked it was one line: print the length of the value read from storage and compare it with the length you expect. The numbers 128 against 842 closed every other suspicion at once. Once it was clear that what came out of storage was not the whole token, the header and Origin were no longer worth arguing about.
A minimal example for the Keychain, not a quote from my script, with the same placeholders:
security find-generic-password -a account -s service -w | tr -d '\n' | wc -ctr -d '\n' makes sure a newline at the end of the output is not counted. A JWT contains only ASCII characters, so the byte count from wc -c equals its character count. A length is also safe to show in a terminal, because the token itself never gets printed.
Reverse the direction: the Keychain only holds a key
There are two shortcuts from here, and both are wrong. Do not put a long secret in the Keychain through the prompt, because that is exactly where it gets cut. Moving it to -w <value> is no way out either, since command arguments are visible to other processes, and that was the reason for choosing the prompt in the first place.
The shape I wrote down as the answer reverses the direction. The Keychain holds a random 32-byte key. In hex each byte takes two characters, so the key is 32 times 2, which is 64 characters, half of the 128 limit and safely below it. That size also fits AES-256, which uses a 256-bit key, or 32 bytes. The secret itself lives in a file encrypted with AES-256-GCM, with mode 0600, written atomically through a rename.
Those last two terms deserve a word. Mode 0600 means only the file's owner can read and write it. Writing atomically through a rename means the new content goes into a temporary file first, and that file is then renamed over the destination, so there is never a moment when the destination holds a half-written file.
This shape is also stronger than the common pattern of deriving the key from the machine name and the user name. Material like that is not secret, and anyone already on the machine can guess it. A random key has nothing to guess from.
A read-back gate
The recipe has one more part that is not optional: after writing, read it back, compare it with what was stored, and fail if they differ. The reason sits in the same note. Storage that can fail silently has to be proven every time, not trusted.
A minimal version of that gate for the Keychain, with neutral placeholders, not my tool's code:
back=$(security find-generic-password -a account -s service -w)
if [ "$back" != "$VALUE" ]; then
echo "read-back mismatch: ${#back} vs ${#VALUE} characters" >&2
exit 1
fiThe failure message prints lengths, not contents, for the same reason as above. Had a gate like this been sitting behind the store command, the 842-character token would have failed the moment it was stored, because 128 characters are not 842 characters, instead of surfacing far downstream as a 401.
What has changed, and what is not proven yet
The tool's project note describes its storage in the present tense: a random 32-byte key in the Keychain, the contents in an AES-256-GCM file with mode 0600, and a pointer back to the note about this 128 limit as the reason.
What none of my notes contain yet is the proof. There is no number showing the 842-character token reading back intact from the new storage, and that storage line mentions neither the read-back gate nor the atomic write through a rename. So the honest summary is this: the storage shape has changed, and two parts of the recipe are not recorded as being in place. By my own rule from the previous section, the new shape cannot be trusted either until a read-back is shown to match what was stored.