From fc347e2b99d8f97a4c957efdad0b6e0f570ae315 Mon Sep 17 00:00:00 2001
From: Alec Reynolds <1153738+reynoldsalec@users.noreply.github.com>
Date: Tue, 12 May 2026 09:40:07 -0700
Subject: [PATCH 1/4] feat: add system-level plugin-auth.json support
Lando now reads and merges a system-level plugin-auth.json in addition to
the user-level ~/.lando/plugin-auth.json. The system file acts as a base
that user config overrides, enabling IT/MDM-managed machines to deploy
registry credentials once for all users.
System paths: /Library/Application Support/Lando/ (macOS),
/srv/lando/ (Linux), %PROGRAMDATA%\Lando\ (Windows).
---
docs/cli/plugin-login.md | 2 +-
docs/guides/hosting-private-plugin.md | 276 ++++++++++++++++++++++++++
lib/cli.js | 1 +
lib/lando.js | 6 +-
tasks/plugin-add.js | 2 +-
utils/get-plugin-config.js | 9 +-
6 files changed, 286 insertions(+), 10 deletions(-)
create mode 100644 docs/guides/hosting-private-plugin.md
diff --git a/docs/cli/plugin-login.md b/docs/cli/plugin-login.md
index af24d97f0..fe906fc23 100644
--- a/docs/cli/plugin-login.md
+++ b/docs/cli/plugin-login.md
@@ -7,7 +7,7 @@ description: lando plugin-login logs you into a valid lando plugin registry
Logs into a Lando plugin registry.
-This will authorize your Lando installation against a Lando plugin registry. A "Lando Plugin Registry" is any `npm` compatible regsitry eg:
+This will authorize your Lando installation against a Lando plugin registry and write the resulting credentials to `~/.lando/plugin-auth.json`. A "Lando Plugin Registry" is any `npm` compatible regsitry eg:
* `https://registry.npmjs.org`
* `https://npm.pkg.github.com`
diff --git a/docs/guides/hosting-private-plugin.md b/docs/guides/hosting-private-plugin.md
new file mode 100644
index 000000000..3dba4089d
--- /dev/null
+++ b/docs/guides/hosting-private-plugin.md
@@ -0,0 +1,276 @@
+---
+title: Hosting and Distributing a Private Lando Plugin
+description: Learn how to build, publish, and distribute a private Lando plugin using a private npm-compatible registry like GitHub Packages.
+
+authors:
+ - name: Team Lando
+ pic: https://gravatar.com/avatar/c335f31e62b453f747f39a84240b3bbd
+ link: https://twitter.com/devwithlando
+updated:
+ timestamp: 1778457600000
+
+mailchimp:
+ # action is required
+ action: https://dev.us12.list-manage.com/subscribe/post?u=59874b4d6910fa65e724a4648&id=613837077f
+ # everything else is optional
+ title: Want similar content?
+ byline: Signup and we will send you a weekly blog digest of similar content to keep you satiated.
+ button: Sign me up!
+---
+
+# Hosting and Distributing a Private Lando Plugin
+
+This guide walks through publishing a Lando plugin to a private npm-compatible registry and distributing it to developer machines — including how to handle authentication so `lando plugin-add` and automatic update checks both work seamlessly.
+
+[[toc]]
+
+## 1. Choose a Registry
+
+| Option | Best for |
+|--------|----------|
+| **GitHub Packages** | Orgs already on GitHub — free for private repos with GitHub Enterprise or limited free tier |
+| **Verdaccio** | Self-hosted, lightweight, zero cost |
+| **Nexus / Artifactory** | Enterprise environments with existing artifact infrastructure |
+
+The instructions below use GitHub Packages (`npm.pkg.github.com`) as the example, but the pattern is identical for other registries — only the registry URL and auth method differ.
+
+## 2. Prepare Your Plugin Package
+
+Your plugin needs three things to be recognized and loaded by Lando.
+
+### package.json
+
+Your `package.json` must have either a `lando` property or `"lando-plugin"` in `keywords`:
+
+```json
+{
+ "name": "@myorg/my-lando-plugin",
+ "version": "1.0.0",
+ "keywords": ["lando-plugin"],
+ "lando": {},
+ "main": "index.js",
+ "publishConfig": {
+ "registry": "https://npm.pkg.github.com"
+ }
+}
+```
+
+The `@myorg` scope must match your GitHub organization name when using GitHub Packages.
+
+### plugin.yml
+
+Every plugin **must** include a `plugin.yml` at its root. Lando's plugin loader filters out any plugin installed to a path matching `plugins/lando-*` unless this file exists — without it the plugin is silently ignored.
+
+```yaml
+name: "@myorg/my-lando-plugin"
+
+api: 3
+is-updateable: true
+```
+
+### Task file
+
+For this demo, we're going to publish a simple command that outputs `Hello World`. Tasks live in a `tasks/` subdirectory. Each file exports a function that receives the `lando` instance:
+
+```javascript
+// tasks/my-command.js
+'use strict';
+
+module.exports = lando => ({
+ command: 'my-command',
+ describe: 'Does something useful',
+ usage: '$0 my-command',
+ level: 'tasks',
+ run: async () => {
+ console.log('Hello World');
+ },
+});
+```
+
+### Minimum file structure
+
+```
+my-lando-plugin/
+├── package.json # must have "lando": {} or lando-plugin keyword
+├── plugin.yml # required — gates plugin discovery
+├── index.js # minimal: module.exports = async lando => {};
+└── tasks/
+ └── my-command.js
+```
+
+## 3. Publish the Plugin
+
+```bash
+# Authenticate as a publisher (needs write:packages scope)
+npm login --registry=https://npm.pkg.github.com --scope=@myorg
+
+# Publish
+npm publish
+```
+
+For CI/CD, use a GitHub Actions token or a service account PAT:
+
+```bash
+npm publish --registry=https://npm.pkg.github.com
+```
+
+## 4. Distribute Credentials to Developer Machines
+
+Lando reads registry credentials from two `plugin-auth.json` files and merges them — the system-level file is the base and the user-level file overrides it:
+
+| Scope | Path |
+|-------|------|
+| **System** (all users on the machine) | macOS: `/Library/Application Support/Lando/plugin-auth.json`
Linux: `/srv/lando/plugin-auth.json`
Windows: `%PROGRAMDATA%\Lando\plugin-auth.json` |
+| **User** (current user only) | `~/.lando/plugin-auth.json` |
+
+Both files use the same key format as npm's config, translated to JSON:
+
+```json
+{
+ "@myorg:registry": "https://npm.pkg.github.com",
+ "//npm.pkg.github.com/:_authToken": "ghp_TOKENHERE"
+}
+```
+
+- `@myorg:registry` — tells Lando (via pacote) which registry to use for `@myorg`-scoped packages
+- `//npm.pkg.github.com/:_authToken` — the auth token for that registry (nerfdart format)
+
+### Option A: Onboarding script (recommended)
+
+Write a shell script that drops the file, substituting a token from a secret manager or environment variable:
+
+```bash
+#!/usr/bin/env bash
+# onboard-lando-plugin.sh
+# Requires: GITHUB_PACKAGES_TOKEN set in environment or passed as $1
+
+TOKEN="${1:-$GITHUB_PACKAGES_TOKEN}"
+LANDO_DIR="${HOME}/.lando"
+AUTH_FILE="${LANDO_DIR}/plugin-auth.json"
+
+mkdir -p "$LANDO_DIR"
+
+cat > "$AUTH_FILE" <Linux: `/srv/lando/plugin-auth.json`
Windows: `%PROGRAMDATA%\Lando\plugin-auth.json` |
+| One-time install | `lando plugin-add @myorg/my-lando-plugin && lando --clear` |
+| Ongoing updates | Automatic — no extra steps |
diff --git a/lib/cli.js b/lib/cli.js
index a37eccb38..4c0b288b3 100644
--- a/lib/cli.js
+++ b/lib/cli.js
@@ -187,6 +187,7 @@ module.exports = class Cli {
packaged,
pluginConfig: {},
pluginConfigFile: path.join(this.userConfRoot, 'plugin-auth.json'),
+ systemPluginConfigFile: path.join(getSysDataPath('lando'), 'plugin-auth.json'),
pluginDirs: [
// src locations
{path: path.join(srcRoot, 'node_modules', '@lando'), subdir: '.', namespace: '@lando'},
diff --git a/lib/lando.js b/lib/lando.js
index 3403e5c08..f8d028a64 100644
--- a/lib/lando.js
+++ b/lib/lando.js
@@ -218,7 +218,7 @@ module.exports = class Lando {
agent: this.config.userAgent,
channel: this.config.channel,
cli: _.get(this, 'config.cli'),
- config: getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig),
+ config: getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig, this.config.systemPluginConfigFile),
debug: require('../utils/debug-shim')(this.log),
});
@@ -477,7 +477,7 @@ module.exports = class Lando {
const Plugin = require('../components/plugin');
// reset Plugin static defaults for v3 purposes
- Plugin.config = getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig);
+ Plugin.config = getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig, this.config.systemPluginConfigFile);
Plugin.debug = require('../utils/debug-shim')(this.log);
// attempt to compute the destination to install the plugin
@@ -522,7 +522,7 @@ module.exports = class Lando {
const Plugin = require('../components/plugin');
// reset Plugin static defaults for v3 purposes
- Plugin.config = getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig);
+ Plugin.config = getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig, this.config.systemPluginConfigFile);
Plugin.debug = require('../utils/debug-shim')(this.log);
// attempt to compute the destination to install the plugin
diff --git a/tasks/plugin-add.js b/tasks/plugin-add.js
index 33a7cf653..654eccb91 100644
--- a/tasks/plugin-add.js
+++ b/tasks/plugin-add.js
@@ -59,7 +59,7 @@ module.exports = lando => {
// normalize incoming options on top of any managed or user plugin config we already have
options.config = merge({}, [
- getPluginConfig(lando.config.pluginConfigFile, lando.config.pluginConfig),
+ getPluginConfig(lando.config.pluginConfigFile, lando.config.pluginConfig, lando.config.systemPluginConfigFile),
lopts2Popts(options),
]);
diff --git a/utils/get-plugin-config.js b/utils/get-plugin-config.js
index ac0d07086..7ec37c3a6 100644
--- a/utils/get-plugin-config.js
+++ b/utils/get-plugin-config.js
@@ -4,9 +4,8 @@ const fs = require('fs');
const merge = require('lodash/merge');
const read = require('./read-file');
-module.exports = (file, config = {}) => {
- // if config file exists then rebase config on top of it
- if (fs.existsSync(file)) return merge({}, read(file), config);
- // otherwise return config alone
- return config;
+module.exports = (file, config = {}, systemFile = null) => {
+ const base = systemFile && fs.existsSync(systemFile) ? read(systemFile) : {};
+ if (fs.existsSync(file)) return merge({}, base, read(file), config);
+ return merge({}, base, config);
};
From 8d88b4e85b41d7bad647747d7d6787a4b648d932 Mon Sep 17 00:00:00 2001
From: Alec Reynolds <1153738+reynoldsalec@users.noreply.github.com>
Date: Tue, 12 May 2026 09:50:39 -0700
Subject: [PATCH 2/4] fix: handle unreadable system plugin-auth.json and
clarify logout scope
Wrap system file read in try/catch so a permission-denied error doesn't
crash Lando at startup. Also note in plugin-logout docs that the command
only clears user-level credentials, not system-level ones.
---
docs/cli/plugin-logout.md | 6 +++++-
utils/get-plugin-config.js | 9 ++++++++-
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/docs/cli/plugin-logout.md b/docs/cli/plugin-logout.md
index 2167c7a3c..e3095323f 100644
--- a/docs/cli/plugin-logout.md
+++ b/docs/cli/plugin-logout.md
@@ -5,7 +5,11 @@ description: lando plugin-logout Logs you out of all active sessions established
# lando plugin-logout
-Logs you out of _all_ active sessions established by `lando plugin-login`
+Logs you out of _all_ active sessions established by `lando plugin-login` by clearing `~/.lando/plugin-auth.json`.
+
+::: warning System-level credentials are not affected
+If credentials were deployed to the system-level `plugin-auth.json` (by an IT/MDM process), this command will not remove them. Those must be removed manually from the system path.
+:::
## Usage
diff --git a/utils/get-plugin-config.js b/utils/get-plugin-config.js
index 7ec37c3a6..27d7f1b36 100644
--- a/utils/get-plugin-config.js
+++ b/utils/get-plugin-config.js
@@ -5,7 +5,14 @@ const merge = require('lodash/merge');
const read = require('./read-file');
module.exports = (file, config = {}, systemFile = null) => {
- const base = systemFile && fs.existsSync(systemFile) ? read(systemFile) : {};
+ let base = {};
+ if (systemFile && fs.existsSync(systemFile)) {
+ try {
+ base = read(systemFile);
+ } catch {
+ // system file unreadable (e.g. wrong permissions) — skip silently
+ }
+ }
if (fs.existsSync(file)) return merge({}, base, read(file), config);
return merge({}, base, config);
};
From b09f42c05ef419563fc1f91c3bb82918b71ac982 Mon Sep 17 00:00:00 2001
From: Alec Reynolds <1153738+reynoldsalec@users.noreply.github.com>
Date: Tue, 12 May 2026 09:55:26 -0700
Subject: [PATCH 3/4] test: add Leia test for system-level plugin-auth.json
credentials
Verifies that a private plugin can be installed using only credentials in
/srv/lando/plugin-auth.json (no user-level ~/.lando/plugin-auth.json),
exercising the new system-level auth merge. Test runs before plugin-login
so there is no user-level file present to mask a failure.
---
examples/plugins/README.md | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/examples/plugins/README.md b/examples/plugins/README.md
index 50a493ef3..397d158f6 100644
--- a/examples/plugins/README.md
+++ b/examples/plugins/README.md
@@ -58,6 +58,16 @@ lando config | grep -q "plugins/@lando/php"
lando plugin-remove "@lando/php"
lando config | grep -qv "plugins/@lando/php"
+# Should be able to add a private plugin using only system-level plugin-auth.json credentials
+sudo mkdir -p /srv/lando
+echo "{\"@lando:registry\":\"https://npm.pkg.github.com\",\"//npm.pkg.github.com/:_authToken\":\"$GITHUB_PAT\"}" | sudo tee /srv/lando/plugin-auth.json > /dev/null
+lando config | grep -qv "plugins/@lando/lando-plugin-test"
+lando plugin-add "@lando/lando-plugin-test"
+lando config | grep -q "plugins/@lando/lando-plugin-test"
+lando plugin-remove "@lando/lando-plugin-test"
+lando config | grep -qv "plugins/@lando/lando-plugin-test"
+sudo rm /srv/lando/plugin-auth.json
+
# Should execute `lando plugin-login`
lando plugin-login --registry "https://npm.pkg.github.com" --password "$GITHUB_PAT" --username "rtfm-47" --scope "lando::registry=https://npm.pkg.github.com"
From ac421444c6c380a668470bd3f2ad65ed83151b74 Mon Sep 17 00:00:00 2001
From: Alec Reynolds <1153738+reynoldsalec@users.noreply.github.com>
Date: Tue, 12 May 2026 09:59:43 -0700
Subject: [PATCH 4/4] docs: update CHANGELOG for system-level plugin-auth.json
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e8a85cd5..a04e5d39e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
## {{ UNRELEASED_VERSION }} - [{{ UNRELEASED_DATE }}]({{ UNRELEASED_LINK }})
+* Added system-level `plugin-auth.json` support so IT/MDM-managed machines can deploy private registry credentials once for all users (macOS: `/Library/Application Support/Lando/plugin-auth.json`, Linux: `/srv/lando/plugin-auth.json`, Windows: `%PROGRAMDATA%\Lando\plugin-auth.json`)
+
## v3.26.4 - [April 28, 2026](https://github.com/lando/core/releases/tag/v3.26.4)
* Fixed `lando ssh` defaulting to a v3 service instead of the v4 `appserver` in mixed-api apps [#461](https://github.com/lando/core/pull/461)