PHPackages                             mnapoli/dotlock - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. mnapoli/dotlock

ActiveLibrary[Security](/categories/security)

mnapoli/dotlock
===============

Secure secrets for local Laravel development

0.1.0(today)51↑2900%MITPHPPHP ^8.4CI passing

Since Jul 31Pushed today1 watchersCompare

[ Source](https://github.com/mnapoli/dotlock)[ Packagist](https://packagist.org/packages/mnapoli/dotlock)[ GitHub Sponsors](https://github.com/sponsors/mnapoli)[ RSS](/packages/mnapoli-dotlock/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (9)Versions (2)Used By (0)

 [![dotlock](./art/illustration.png)](./art/illustration.png)

The problem
-----------

[](#the-problem)

Some projects need secrets to work locally: an OpenAI key, a Stripe test key, a sandbox API token password…

They sit in `.env` or `~/.bashrc` in plain text. Every process on your machine can read them: a malicious dependency, a rogue agent, a compromised application.

The solution
------------

[](#the-solution)

Dotlock moves secrets to `.env.secrets`, a gitignored file that looks like `.env` but the values are encrypted with a passphrase you choose:

```
# .env.secrets
OPENAI_API_KEY="mvGOEc0Vf1Xh0…"
STRIPE_SECRET="Tir/AHfuTgKoS…"
```

Now run `php artisan dev`:

```
php artisan dev

   2 encrypted secrets to unlock.

 ┌ Passphrase ──────────────────────────────────────────────────┐
 │                                                              │
 └──────────────────────────────────────────────────────────────┘
```

Dotlock prompts for your passphrase, decrypts the values, and the usual Laravel dev processes start with the secrets in their environment variables. Secrets are never written to disk.

**Laravel then runs as usual**. Nothing else in the application changes.

Note

Dotlock works with `php artisan dev`, it doesn't work with Herd or Valet. Their PHP-FPM is started by the OS, not by your shell, so we cannot inject the secrets into their environment.

Install
-------

[](#install)

```
composer require --dev mnapoli/dotlock
```

Add this at the top of `bootstrap/app.php`, before `Application::configure()`:

```
// Load encrypted secrets into the environment, before config is built.
// Does nothing unless APP_SECRETS_KEY is set and `.env.secrets` exists.
if (class_exists(\Dotlock\Secrets::class)) {
    \Dotlock\Secrets::unlock(dirname(__DIR__));
}
```

Then add the file to `.gitignore`:

```
.env.secrets

```

You *could* commit it to share the secrets with teammates, but you'd have to share the passphrase and everyone would share the same secrets.

Usage
-----

[](#usage)

Import a secret from `.env` or add a new one:

```
php artisan secrets:set

 ┌ Which environment variable? ─────────────────────────────────┐
 │ API_TOKEN                                                  ⌄ │
 └──────────────────────────────────────────────────────────────┘
  Names in .env and names already encrypted are both offered. A new one is fine too.

 ┌ Value for API_TOKEN ─────────────────────────────────────────┐
 │ ••••••••••••••••••                                           │
 └──────────────────────────────────────────────────────────────┘

 ┌ Passphrase ──────────────────────────────────────────────────┐
 │ •••••••••                                                    │
 └──────────────────────────────────────────────────────────────┘
```

If the secret was already in `.env`, it is moved to `.env.secrets`. The passphrase is your password that unlocks everything, it is never stored anywhere, remember it!

Leave non-secret environment variables in `.env`, `.env.secrets` is only for secrets.

Start working:

```
php artisan dev
```

Other commands:

Command`secrets:list`List the secret names, not the values`secrets:get NAME`Retrieve a secret value`secrets:forget NAME`Forget a secret`secrets:rekey`Change the passphrase without rewriting a single valueFor non-interactive use (scripts, a shell you trust…), export the passphrase in `APP_SECRETS_KEY` (every artisan command unlocks the secrets automatically):

```
export APP_SECRETS_KEY="your passphrase"
```

You can also read it from your password manager rather than typing it into shell history, e.g. with 1Password:

```
export APP_SECRETS_KEY="$(op read op://Private/my-app/passphrase)"
```

Limits
------

[](#limits)

- **`config:cache` and `optimize` are forbidden while a `.env.secrets` exists.** Caching config resolves every `env()` call and writes the answers to `bootstrap/cache/config.php` in plain text. That would put your secrets on disk in plaintext, so Dotlock blocks it.
- **Tests don't unlock secrets.** When `APP_ENV` is `testing`, `.env.secrets` are never read even with the passphrase in the environment.
- **Commands without the passphrase don't have the secrets.** E.g. if Claude Code runs an artisan command, the environment variables of `.env.secrets` will be empty in Laravel. The `artisan` command will work as long as it doesn't need the secrets (e.g. `php artisan migrate`, etc.) so most of the time there is no problem.
- **Requests served by Herd or Valet do not unlock secrets.** Their PHP-FPM is started by the OS, not by your shell, so it never sees `APP_SECRETS_KEY`.

How it works
------------

[](#how-it-works)

- The passphrase is stretched with **Argon2id** into a key that opens one thing: a random **master key** kept encrypted in the file's header. The master key encrypts the values with **XSalsa20-Poly1305** (libsodium's `secretbox`). Changing the passphrase rewrites one line, not every value.
- Encryption is authenticated, so a wrong passphrase and a corrupted file are detected outright, the vault can never hand back plausible-looking garbage.
- Values are base64 on one line whatever they contain, so the vault can hold variables with any value (unlike `.env`): a multi-line PEM key, a value with quotes or `#` in it.
- The file is `chmod 0600`, and the passphrase is never stored anywhere. There is no recovery if the passphrase is lost.

Alternatives
------------

[](#alternatives)

**`php artisan env:encrypt`**

It encrypts the whole `.env` into `.env.encrypted` meant to be committed and decrypted on deploy. It solves prod secrets, not your local machine: to use it you run `env:decrypt`, and the plaintext `.env` is back on disk. Dotlock encrypts value by value, keeps the names readable, and the decrypted values only ever exist in process memory.

**1Password CLI**

In theory, you could set `API_TOKEN="$(op read op://Private/my-app/token)"` in `.env`, but that doesn't work out of the box (`.env` files are parsed, not executed). To make it work, you have to wrap every command with `op run` (`op` is the 1Password CLI). For example `op run --env-file .env -- php artisan tinker`. This is a pain to remember and type. On top of that, some values live in `.env` and some in the 1Password vault, which is not the best DX.

**Apple Keychain**

The Apple Keychain has a CLI to read secrets, but it is macOS-only and has no integration with `.env` or Laravel. You'd have to run commands like `API_TOKEN="$(security find-generic-password -s my-app -a api-token -w)" php artisan tinker`. And secrets stored this way can be read back silently by any process running as your user, the Keychain doesn't prompt when a malicious script shells out to the same command.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 85.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/329a6111724074f5388e95dd41a03ccf3c43f4bfe1ecf27c94c9efc6f7823228?d=identicon)[mnapoli](/maintainers/mnapoli)

---

Top Contributors

[![mnapoli](https://avatars.githubusercontent.com/u/720328?v=4)](https://github.com/mnapoli "mnapoli (18 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (3 commits)")

---

Tags

dotenvlaravellaravel-packagesecretslaravelencryptionenvdotenvsecretscredentials

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/mnapoli-dotlock/health.svg)

```
[![Health](https://phpackages.com/badges/mnapoli-dotlock/health.svg)](https://phpackages.com/packages/mnapoli-dotlock)
```

###  Alternatives

[laravel/octane

Supercharge your Laravel application's performance.

4.0k28.5M248](/packages/laravel-octane)[statamic/cms

The Statamic CMS Core Package

4.9k3.8M1.1k](/packages/statamic-cms)[code16/sharp

Laravel Content Management Framework

79266.1k10](/packages/code16-sharp)[typicms/base

A modular multilingual CMS built with Laravel, enabling developers to manage structured content like pages, news, events, and more.

1.6k20.4k](/packages/typicms-base)[ecotone/laravel

Ecotone for Laravel — CQRS, Event Sourcing, Sagas, Durable Workflows, and Outbox on top of Laravel Queue, via PHP attributes.

21327.3k3](/packages/ecotone-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
