Extending Helm with plugins: the plugin system architecture and its storage directories, popular plugins (helm-diff, helm-secrets, helm-unittest, helm-git, helm-s3, helm-push), installing and managing plugins, and creating custom plugins with plugin.yaml and executable scripts.

After episode 21, where we covered multi-environment management — from values file organization to Helmfile declaring all releases — in this episode we extend Helm itself via plugins. Helm 3 was deliberately designed as a lean core: it handles install, upgrade, package, and repo, but not everything. Diff before apply? Not in Helm's core. Secret encryption? No. Template unit testing? No. All of that is filled in by the plugin ecosystem.
Think of Helm like a smartphone operating system: the kernel is small and stable, but what makes it useful are the apps installed on top. Helm plugins are those apps — additional commands attached to the CLI, invoked like helm diff, helm secrets, helm unittest, and automatically appearing in helm help as new subcommands.
Why should you care? Because serious production workflows almost always involve plugins: helm-diff saves you from a wrong upgrade by showing the changes before they're applied, helm-secrets keeps sensitive values encrypted in Git, and helm-unittest makes chart templates something testable in CI without a cluster. Not mastering plugins means rebuilding everything manually — or doing upgrades without being able to see what will change.
In this episode we dissect the plugin system architecture, the most important plugins in the ecosystem, how to install and manage them, and then — the most interesting part — how to create your own plugin with a plugin.yaml manifest, executable scripts, and the HELM_* environment variables Helm provides.
A Helm plugin is a simple concept: a directory with a plugin.yaml manifest file and one or more executable files. When Helm runs helm <plugin-name> <args>, it finds the plugin directory, reads plugin.yaml to locate the correct command, then executes it — passing through the arguments and adding a number of special environment variables.
The plugin directory structure on a system:
~/.local/share/helm/plugins/
├── diff/
│ ├── plugin.yaml
│ └── scripts/
│ └── diff
├── secrets/
│ ├── plugin.yaml
│ └── scripts/
│ └── run
└── unittest/
├── plugin.yaml
└── runThis directory location follows the XDG Base Directory — on Linux it's usually ~/.local/share/helm/plugins, but Helm honors the HELM_PLUGINS variable if you change it. Some distributions (and users using sudo) may place it in the root user's directory. To check where your plugins are:
helm env | grep HELM_PLUGINS
helm plugin listPlugin discovery happens when Helm scans that directory on every invocation. Each subdirectory containing a valid plugin.yaml is considered one plugin, and its defined commands are registered to the CLI. That means, to "install" a plugin you don't need to modify Helm at all — just place the right directory, and Helm immediately recognizes it.
The Helm ecosystem has six plugins that almost always appear in production workflows:
helm-diff — arguably the most important plugin. It runs helm upgrade --dry-run and formats the difference between the currently running manifests and what will be applied, in an easy-to-read diff format. It's the "safety window" before every upgrade:
helm diff upgrade myapp ./charts/myapp \
-f deploy/values-common.yaml \
-f deploy/values-prod.yamlIts output looks like git diff, so you can see exactly which lines change, get added, or get deleted:
~ Deployment "myapp" changed
# Source: myapp/templates/deployment.yaml
23 spec:
24 template:
25 spec:
26 containers:
27 - name: myapp
+ resources:
+ requests:
+ cpu: 500m
+ memory: 512Mi
+ limits:
+ cpu: "2"
+ memory: 2GiIn CI/CD, helm diff is often used as a gate: the pipeline shows the diff in the review log, and a human (or an automated rule) approves before helm upgrade actually runs.
helm-secrets — integration with SOPS to encrypt values files before they go into Git. Secrets are still written in values.yaml, but in encrypted form that can only be read with the right key (KMS, Age, PGP). Basic usage: helm secrets enc secrets/prod.yaml encrypts, helm secrets dec decrypts it into a temporary file Git ignores, and helm secrets upgrade combines the process. Advantage: Git still holds the secret, but in a form useless without the key — bridging the review need and the security need.
helm-unittest — unit testing for chart templates without a cluster. It writes tests in YAML and validates the rendered output: "Deployment must have 2 replicas if value X", "Ingress must not be rendered if ingress.enabled: false". This is the plugin we mentioned in episode 14 and it's the backbone of chart testing in CI.
helm-git — lets charts be referenced directly from a Git repository: helm install app helm-git://https://github.com/org/charts.git//path/to/chart?ref=v1.2.3. Useful for teams that don't yet have a formal chart repository and want to use a Git branch as the chart source.
helm-s3 — makes an S3 bucket (or compatible, including MinIO) a chart repository. helm repo add myrepo s3://my-charts-bucket then helm s3 push to upload charts. A popular solution for private repositories on AWS.
helm-push — uploads charts to ChartMuseum (the chart repository we discussed in episode 17) via its API: helm push mychart-1.0.0.tgz http://chartmuseum.internal. Before the OCI era, this was the standard way to publish organizational charts.
helm-s3 and helm-push vs OCI — it's worth noting their historical position: both plugins were born before Helm supported OCI. Since OCI matured (episode 18), the helm push flow to a container registry replaces the need for both in many teams — one toolchain, one auth, no plugins. They remain relevant in organizations that have already built internal HTTP/S3 repositories, or that aren't ready to move to a registry. The migration decision follows the same logic as the repository migration in episode 18.
Warning
Plugin security is your responsibility. A Helm plugin is code executed with the shell privileges of the user who invokes it — not a sandbox. Installing a plugin from an unknown repository is the same as running arbitrary scripts on your work machine. Verify the source, only install plugins from trusted maintainers, pin versions (--version), and audit plugin.yaml plus its scripts before team-wide use. This policy becomes even more important on CI runners that have access to production credentials.
All plugin operations use the helm plugin subcommand:
helm plugin install https://github.com/databus23/helm-diff
helm plugin install https://github.com/jkroepke/helm-secrets --version v4.5.1
helm plugin list
helm plugin update diff
helm plugin uninstall diffA few important details. helm plugin install accepts a Git URL, a local path, or a tarball. When installing from Git, Helm loads the plugin.yaml from the repo root and executes the install command defined within (if any) — many plugins use this to download the actual binary. --version selects a specific version, important for reproducible environments. And because plugins are executed binaries, they must be reinstalled when changing architecture or OS — make sure the required plugin list is documented in your team's on-call documentation, or use a CI container image that already contains the plugins.
One nuance rarely discussed: plugins are neither cached nor verified like chart dependencies. There's no Chart.lock for plugins — every developer and every CI runner that needs a plugin must install it themselves. That's why the common convention is to document plugins in a Makefile or bootstrap script (for example, a make plugins target running a series of helm plugin install --version <x>), and bake them into the CI base image. That way, plugin version differences between environments — the classic "works on laptop, fails in CI" bug source — can be minimized.
Now the interesting part — creating a custom plugin. The smallest plugin only needs two files: plugin.yaml and a script. Its manifest:
name: "template-dump"
version: "0.1.0"
usage: "Render all templates and save to a directory"
description: |-
Custom plugin to render a chart into an output directory
so it can be reviewed by non-Helm reviewers.
command: "$HELM_PLUGIN_DIR/scripts/dump.sh"
hooks:
install: "cd $HELM_PLUGIN_DIR && ./scripts/install.sh"
platformCommand:
- os: linux
command: "$HELM_PLUGIN_DIR/scripts/dump-linux.sh"
- os: darwin
command: "$HELM_PLUGIN_DIR/scripts/dump-macos.sh"Let's dissect each field. name must be unique — it's the name appearing in helm plugin list and used as the subcommand (helm template-dump). command is the path to the executable run when the plugin is invoked; notice the use of the $HELM_PLUGIN_DIR variable — an environment variable injected by Helm so the plugin knows where it lives, letting the plugin be moved without breaking paths. hooks.install (and update) are commands executed when helm plugin install runs — the right place to download binaries, compile, or set permissions. platformCommand allows a different command per operating system — important for plugins compiled per platform.
The simplest plugin script — leveraging the environment variables Helm provides:
#!/usr/bin/env bash
set -euo pipefail
RELEASE="${1:?Usage: helm template-dump <release>}"
CHART="${2:?Usage: helm template-dump <release> <chart>}"
OUTPUT="${3:-./rendered}"
mkdir -p "$OUTPUT"
"$HELM_BIN" template "$RELEASE" "$CHART" \
--namespace "$HELM_NAMESPACE" \
-f "${HELM_VALUES_FILE:-values.yaml}" \
> "$OUTPUT/$RELEASE.yaml"
echo "Templates dumped to $OUTPUT/$RELEASE.yaml (namespace: $HELM_NAMESPACE)"This script demonstrates the most useful HELM_* environment variables. $HELM_BIN holds the path to the Helm binary that invoked the plugin — always use this to call Helm from within a plugin, don't hardcode helm, so the plugin keeps working if Helm runs from a non-standard path. $HELM_NAMESPACE holds the currently used namespace (from --namespace or the HELM_NAMESPACE environment). There's also $HELM_KUBECONTEXT, $HELM_REGISTRY_CONFIG, $HELM_REPOSITORY_CONFIG, $HELM_PLUGIN_DIR, and $HELM_DEBUG — the complete list is in helm env.
Once the files are ready, make it executable and test:
chmod +x scripts/dump.sh
helm plugin install .
helm template-dump myapp ./charts/myapp ./out
helm plugin listTip
Debugging plugins. When a plugin errors, run helm template-dump ... --debug — Helm sets $HELM_DEBUG so your script can print extra output useful for troubleshooting. A common pattern: if [[ "${HELM_DEBUG:-}" == "1" ]]; then set -x; fi at the top of the script. Also remember that a plugin runs as a subprocess — it doesn't share environment or shell state with the main Helm, so everything needed must come through HELM_* variables or arguments.
Writing a good plugin takes more than a working script. Here are the standards to meet so your plugin can be used by others (or by yourself six months later):
plugin.yaml about the minimum supported Helm version (minimumHelmVersion isn't a standard field, but document it in the description) and the targeted Kubernetes version. A plugin using new flags in Helm 3.12 will fail mysteriously on Helm 3.10.set -euo pipefail — stop on error, reject unset variables, and fail if any command in a pipeline errors. Error output must tell the user what went wrong and how to fix it, not just "command not found".usage and description in plugin.yaml should follow a helpful format: concise usage with arguments, a description explaining why this plugin exists. A README with usage examples is a must for distribution.plugin.yaml at the root and an install.sh script in hooks.install that downloads binaries from GitHub Releases matching the architecture — the pattern used by helm-diff, helm-secrets, and almost every major plugin.In this episode 22 we dissected the Helm plugin system: the architecture based on a simple directory with a plugin.yaml manifest and executables, six popular plugins you must master (helm-diff for diff-before-apply, helm-secrets for SOPS encryption, helm-unittest for testing, helm-git, helm-s3, and helm-push), managing plugins with helm plugin install/list/update/uninstall, and how to create a custom plugin — the manifest, scripts, the HELM_BIN and HELM_NAMESPACE environment variables, and distribution best practices.
The core takeaways:
helm diff before every upgrade is a discipline that saves production.$HELM_BIN and $HELM_PLUGIN_DIR are the bridge between plugin and Helm — always use both.platformCommand for multi-OS, hooks.install for setup, set -euo pipefail for reliability.In the next episode, episode 23, we cover the Helm SDK & programmatic usage: no longer calling Helm as a CLI, but importing Helm as a Go library inside your own applications — the foundation of operators, platform engineering tools, and self-service portals. See you in episode 23!