← Back to blog

My Wife "Hacked" My MacBook, So I Wrote a Laravel Package

~8 min read

I was sitting in the living room, MacBook on my lap, half watching my son, half doing what developers do. Pretending to be productive while actually deep in a rabbit hole that has nothing to do with the task at hand.

Then things got weird.

Apps started opening on their own. Random text appeared in my terminal. Characters I’d never typed, in patterns that made no sense. The mouse cursor drifted across the screen like it had its own agenda. Windows were resizing. Spotlight popped open. Something was typing into it.

My heart rate doubled. Someone got into my machine. Remote access. A trojan. Some zero-day exploit I’d read about on Hacker News and thought “that’ll never happen to me.” All my API keys, database credentials, client secrets. Everything in my .env files, sitting in plaintext on disk, ready to be exfiltrated.

I was about to slam the lid shut and run to my office to check my router logs. I was mentally preparing a drill and a microwave to put this corrupted machine out of its misery. Full scorched earth protocol. No compromised MacBook leaves this house alive.

Then I glanced over at the kitchen counter.

My wife was wiping down my external keyboard and trackpad with a damp cloth.

Not hacked. Just cleaned. The drill went back to the toolbox.

The paranoia that wouldn’t go away

I laughed it off. She laughed. My son is two months old, so he just stared at the ceiling like he always does. But the feeling stuck with me. For about ten seconds, I genuinely believed someone had access to everything on my machine. And the worst part? If they did, they wouldn’t even need to be clever about it.

Every Laravel project has a .env file. Inside it: database passwords, API keys, Stripe secrets, mail credentials. Plaintext. Right there on your filesystem. Sure, it’s in .gitignore, but it’s still a file on disk. Any process, any script, any AI tool that touches your project directory can read it.

And that last point is what really got me thinking.

I use Claude Code daily. I use other AI tools. They’re incredible, and I trust the companies behind them not to intentionally leak my data. But these tools read files. They build context from your codebase. They might log what they process for debugging, for training, for improving the product. Even with the best intentions and strictest policies, every system that touches your data is a surface area.

I don’t store production keys locally. I’m not insane. But sometimes you need to connect to a production service just to debug something. A weird webhook payload, a failing charge, a sync issue you can’t reproduce in sandbox. You paste the key in, fix the thing, and maybe forget to remove it. Now a production key is sitting in your .env file, in plaintext, on disk. That’s not a hypothetical. That’s a Tuesday.

And .env files aren’t the only concern. Logs. Shell history. Debug output. Crash reports. Your secrets pass through more places than you think, and each one is a potential leak. Not a dramatic hack-the-planet leak. A quiet, boring, “someone’s log file got exposed” leak. The kind you never find out about.

So I built a thing

Laravel Keyring stores your secrets in your operating system’s native credential manager instead of .env files. On macOS, that’s the Keychain. On Linux, GNOME Keyring or KWallet via Secret Service. On Windows, Credential Manager.

The idea is simple: your OS already has a secure, encrypted, access-controlled vault for storing credentials. Your browser uses it. Your SSH agent uses it. Why shouldn’t your Laravel app?

The stack

It’s a Laravel package with a driver-based architecture, similar to how Laravel handles mail or cache. One interface, multiple backends:

  • macOS Keychain - uses the security CLI under the hood
  • Linux Secret Service - uses secret-tool (GNOME Keyring / KWallet)
  • Windows Credential Manager - uses cmdkey and PowerShell
  • Env file driver - for CI/CD or Docker where there’s no OS keychain
  • JSON driver - encrypted JSON file using Laravel’s APP_KEY

An auto driver detects your OS and picks the right one. You don’t need to configure anything for local development.

How it works

Store a secret:

php artisan keyring:set STRIPE_SECRET sk_test_abc123

The secret is now in your macOS Keychain (or whatever your OS uses), encrypted and protected by your user account. Not in a file, not in your git history, not readable by any process that doesn’t have keychain access.

Retrieve it:

php artisan keyring:get STRIPE_SECRET
# sk_test_abc123

List all stored keys (never shows values):

php artisan keyring:list
# +---------------+
# | Key           |
# +---------------+
# | STRIPE_SECRET |
# | DB_PASSWORD   |
# +---------------+

The magic: env injection

Here’s the part that makes it actually useful. Keyring can automatically inject secrets into $_ENV, $_SERVER, and putenv() at boot time. This means env('STRIPE_SECRET') just works. Laravel doesn’t know or care that the value came from your keychain instead of .env.

// config/keyring.php
'inject_into_env' => true,

'inject_keys' => [
    'STRIPE_SECRET',
    'DB_PASSWORD',
],

Your .env file can have empty placeholders:

STRIPE_SECRET=
DB_PASSWORD=

At boot, Keyring fills in the real values from the keychain. If a value is already set in $_ENV (like from your .env file or server config), Keyring won’t touch it. No surprises.

This works with Laravel Herd, php artisan serve, and any other way you run Laravel locally.

Migrating existing secrets

Already have a .env full of secrets? Import them in one command:

php artisan keyring:import

It reads your .env, lets you pick which keys to import, and stores them in the keychain. Then you can blank out the values in .env and let Keyring handle them.

The performance tradeoff

Every secret injection spawns a shell process to query the keychain. That’s not free:

ScenarioOverhead per request
Injection disabled-
1 key+27 ms
5 keys+131 ms
20 keys+498 ms

Each key adds roughly 25ms. For local development with a handful of secrets, you won’t notice. For 20 keys, half a second per request is noticeable.

For best results, list only the keys you actually need in inject_keys rather than injecting everything.

APCu cache: zero overhead after the first request

If even a few milliseconds bother you, Keyring now ships with an opt-in APCu cache. APCu stores values in shared memory attached to the PHP-FPM process — so secrets stay in RAM, never touch disk, and survive across requests.

// config/keyring.php
'cache' => [
    'enabled' => true,
    'driver'  => 'apcu',
    'ttl'     => 3600,
],

The first request hits the keychain as usual. Every request after that reads from APCu in microseconds. The shell calls are gone entirely until the TTL expires or you run php artisan keyring:cache:clear.

If you use Laravel Herd, you’ll need to install the APCu PHP extension first. Herd ships its own PHP binaries, so you compile via Homebrew and register it in Herd’s php.ini. The Herd docs on PHP extensions walk through it, but the short version:

brew install php
pecl install apcu
# Then add the extension= line to ~/Library/Application Support/Herd/config/php/<version>/php.ini

Secrets are never written to disk. APCu memory is process-local and disappears when PHP-FPM restarts. Security stays intact, and you get sub-millisecond reads.

Use it in your own project

composer require gause/laravel-keyring --dev

The package auto-discovers its service provider. Publish the config if you want to customize it:

php artisan vendor:publish --tag=keyring-config

Store your secrets, list the keys in inject_keys, and you’re done. Your .env file becomes a harmless template. The real secrets live in a vault that your OS has been protecting since long before Laravel existed.

The actual lesson

The keyboard incident was funny. My wife still brings it up when I’m being paranoid about security. But it accidentally stress-tested something real: my emotional response to “someone might have my secrets.”

If ten seconds of a rogue trackpad made me that anxious, maybe I should stop keeping secrets in plaintext files on disk. Not because my wife is a threat actor. But because the best security is the kind where there’s nothing to steal in the first place.


If you want to hear about more projects like this, and about building SaaS, Laravel, and developer life, subscribe to my newsletter.