kat
kat provides a terminal UI for rendering, validating, and displaying local Kubernetes manifests. It eliminates the frustrating cycle of manually running commands, scrolling through endless output, and constantly losing context when working with Helm charts, Kustomize overlays, and other manifest generators.
β€οΈ Made with bubble tea, glow, and chroma.
β¨ Features
- ποΈ Manifest browsing - Navigate hundreds of rendered manifests with fuzzy search and filtering, no more endless scrolling through terminal output
- β‘οΈ Live reload - Use
--watch to automatically re-render when you modify source files, without losing your current context
- π Error handling - Rendering and validation errors are displayed as overlays and disappear if reloading resolves the error
- π― Project detection - Automatically detect Helm charts, Kustomize projects, and custom manifest generators using powerful CEL expressions
- π§ͺ Tool integration - Define profiles for any manifest generator (Helm, Kustomize, CUE, KCL, Jsonnet, etc.) with pre/post-render hooks
- π Plugin system - Create custom keybind-triggered commands for common tasks that can't run on hooks, like dry-runs or diffs
- β
Custom validation - Run tools like
kubeconform, kyverno, or custom validators automatically on rendered output
- π¨ Beautiful UI - Syntax-highlighted YAML with customizable themes and keybindings that match your preferences
π¦ Installation
Homebrew
brew install macropower/tap/kat
Go
go install github.com/macropower/kat/cmd/kat@latest
Docker
Docker images are published to ghcr.io/macropower.
All images are configured with WORKDIR=/data, so you can mount your current directory there to run kat against your local files.
Run the latest alpine image:
docker run -it -v .:/data -e TERM=$TERM ghcr.io/macropower/kat:latest-alpine
Run the latest debian image:
docker run -it -v .:/data -e TERM=$TERM ghcr.io/macropower/kat:latest-debian
The default config is located at /config/kat/config.yaml, and you can override it by mounting your own configuration file at that path.
There is also a scratch image that contains only the kat binary, which is useful when you want to build your own image (which I generally recommend doing):
FROM alpine:latest
COPY --from=ghcr.io/macropower/kat:latest /kat /usr/local/bin/kat
# Add whatever customization you need here.
ENTRYPOINT ["/usr/local/bin/kat"]
Nix
You can install kat using my NUR.
With nix-env:
nix-env -iA kat -f https://github.com/macropower/nur-packages/archive/main.tar.gz
With nix-shell:
nix-shell -A kat https://github.com/macropower/nur-packages/archive/main.tar.gz
With your flake.nix:
{
inputs = {
macropower.url = "github:macropower/nur-packages";
};
# Reference the package as `inputs.macropower.packages.<system>.kat`
}
With devbox:
devbox add github:macropower/nur-packages#kat
GitHub CLI
gh release download -R macropower/kat -p "kat_$(uname -s)_$(uname -m).tar.gz" -O - | tar -xz
And then move kat to a directory in your PATH.
Curl
curl -s https://api.github.com/repos/macropower/kat/releases/latest | \
jq -r ".assets[] |
select(.name | test(\"kat_$(uname -s)_$(uname -m).tar.gz\")) |
.browser_download_url" | \
xargs curl -L | tar -xz
And then move kat to a directory in your PATH.
Manual
You can download binaries from releases.
π Verification
You can verify the authenticity and integrity of kat releases.
See verification for more details.
π Usage
Show help:
kat --help
Render a project in the current directory:
kat
Render a project and enable watch (live reloading):
kat -w
Render a project in a specific directory:
kat ./example/helm
Render a project in a specific directory using the ks profile:
kat ./example/kustomize ks
Render a project and override the profile arguments:
kat ./example/kustomize ks -- build . --enable-helm
Render a project with command passthrough:
kat ./example/helm task -- helm:render
Render using data from stdin:
cat ./example/kustomize/resources.yaml | kat -f -
βοΈ Configuration
You can use kat --write-config to generate a default configuration file at ~/.config/kat/config.yaml. This file allows you to customize the behavior of kat, such as the UI style, keybindings, rules for project detection, and profiles for rendering different types of projects. This will also write a JSON schema to the same location, for use with your editor's YAML language server.
Alternatively, you can find the default configuration file as well as JSON schemas in pkg/config.
π οΈ Rules and Profiles
You can customize how kat detects and renders different types of projects using rules and profiles in the configuration file. This system uses CEL (Common Expression Language) expressions to provide flexible file matching and processing.
π― Rules
Rules determine which profile should be used. Each rule contains:
match (required): A CEL expression that returns true if the rule should be applied
profile (required): The name of the profile to use when this rule matches
Rules use boolean CEL expressions with access to:
files (list): All file paths in the directory
dir (string): The directory path being processed
rules:
- # Select the Helm profile if any Helm chart files exist
match: >-
files.exists(f, pathBase(f) in ["Chart.yaml", "Chart.yml"])
profile: helm
- # Select the Kustomize profile if any Kustomization files exist
match: >-
files.exists(f, pathBase(f) in ["kustomization.yaml", "kustomization.yml"])
profile: ks
- # Fallback: select the YAML profile if any YAML files exist
match: >-
files.exists(f, pathExt(f) in [".yaml", ".yml"])
profile: yaml
π Profiles
Profiles define how to render projects. They can be automatically selected by rules, or manually specified when kat is invoked. Each profile contains:
command (required): The command to execute
args: Arguments to pass to the command
env: List of environment variables for the command
envFrom: List of sources for environment variables
source: Define which files to watch for changes (when watch is enabled)
ui: UI configuration overrides
hooks: Initialization and rendering hooks
init hooks are executed once when kat is initialized
preRender hooks are executed before the profile's command is run
postRender hooks are executed after the profile's command has run, and are provided the rendered output via stdin
plugins: Custom commands that can be executed on-demand with keybinds
description (required): Human-readable description of what the plugin does
keys (required): Array of key bindings that trigger the plugin
command (required): The command to execute
args: Arguments to pass to the command
Profile source expressions use list-returning CEL expressions with the same variables as rules.
profiles:
helm:
command: helm
args: [template, ., --generate-name]
source: >-
files.filter(f, pathExt(f) in [".yaml", ".yml", ".tpl"])
envFrom:
- callerRef:
pattern: "^HELM_.+"
ui:
theme: dracula
hooks:
init:
- command: helm
args: [version, --short]
preRender:
- command: helm
args: [dependency, build]
envFrom:
- callerRef:
pattern: "^HELM_.+"
postRender:
# Pass the rendered manifests via stdin to `kubeconform`.
- command: kubeconform
args: [-strict, -summary]
plugins:
dry-run:
command: helm
args: [install, ., -g, --dry-run]
envFrom:
- callerRef:
pattern: "^HELM_.+"
description: invoke helm dry-run
keys:
- code: ctrl+r
alias: βr
ks:
command: kustomize
args: [build, .]
source: >-
files.filter(f, pathExt(f) in [".yaml", ".yml"])
env:
- name: KUSTOMIZE_ENABLE_ALPHA_COMMANDS
value: "true"
ui:
compact: true
theme: tokyonight-storm
hooks:
init:
- command: kustomize
args: [version]
π§© CEL Functions
kat provides custom CEL functions for file path operations:
pathBase(string): Returns the filename (e.g., "Chart.yaml")
pathExt(string): Returns the file extension (e.g., ".yaml")
pathDir(string): Returns the directory path
yamlPath(file, path): Reads a YAML file and extracts a value using a YAML path expression
You can combine these with CEL's built-in functions like exists(), filter(), in, contains(), matches(), and logical operators.
Example:
rules:
- match: >-
files.exists(f,
pathBase(f) == "Chart.yaml" &&
yamlPath(f, "$.apiVersion") == "v2")
profile: helm
profiles:
helm:
command: helm
args: [template, ., --generate-name]
source: >-
files.filter(f,
pathExt(f) in [".yaml", ".yml", ".tpl"])
For more details on CEL expressions and examples, see the CEL documentation.
π₯ DRY Configuration
The kat configuration supports YAML anchor nodes, alias nodes, and merge keys. You can define common settings once and reuse them across the configuration.
profiles:
ks: &ks
command: kustomize
args: [build, .]
source: >-
files.filter(f, pathExt(f) in [".yaml", ".yml"])
hooks:
postRender:
- &kubeconform
command: kubeconform
args: [-strict, -summary]
ks-helm:
<<: *ks
args: [build, ., --enable-helm]
helm:
command: helm
args: [template, ., --generate-name]
source: >-
files.filter(f, pathExt(f) in [".yaml", ".yml", ".tpl"])
envFrom:
- callerRef:
pattern: "^HELM_.+"
hooks:
postRender:
- *kubeconform
β€οΈ Thanks to goccy/go-yaml.
π Examples
Default config - By default, kat includes a configuration that supports helm, kustomize, and generic YAML files. This is a great starting point for writing your own custom config:
Support for custom tools - You can add support for other languages/tools like kcl, jsonnet, flux-local, cue, and so on:
rules:
- match: >-
files.exists(f, pathExt(f) == ".k")
profile: kcl
profiles:
kcl:
command: kcl
args: [run, .]
source: >-
files.filter(f, pathExt(f) == ".k")
envFrom:
- callerRef:
pattern: "^KCL_.+"
Content-based detection - Match based on file content, not just names:
rules:
- # Match Helm v3 specifically
match: >-
files.exists(f,
pathBase(f) == "Chart.yaml" &&
yamlPath(f, "$.apiVersion") == "v2")
profile: helm-v3
- # Match Kubernetes native manifests with specific API versions
match: >-
files.exists(f,
pathExt(f) in [".yaml", ".yml"] &&
yamlPath(f, "$.apiVersion") in ["apps/v1", "v1"])
profile: yaml
Using Task - If you use task, you can use your tasks in the kat config:
rules:
- match: >-
files.exists(f, pathBase(f) in ["Taskfile.yml", "Taskfile.yaml"])
profile: task
profiles:
task:
command: task
args: [render]
source: >-
files.filter(f, pathExt(f) in [".yaml", ".yml"])
hooks:
postRender:
- command: task
args: [validate]
Note that you should write your task to:
- Output the rendered manifests to stdout, and anything else to stderr.
- Tolerate being called from any directory in the project.
- E.g., instead of
./folder, use {{joinPath .ROOT_DIR "folder"}}.
- Not require any additional arguments to run.
- You can reference
{{.USER_WORKING_DIR}} to obtain the path that the user invoked kat from/with.
- E.g.,
vars: { PATH: "{{.PATH | default .USER_WORKING_DIR}}" }
If you are concerned about safety (i.e. accidentally calling a task defined by someone else), you can consider not including a rule for task and only allowing it to be invoked manually via the CLI args, or you could write a more narrow match expression (e.g. f.contains("/my-org/")).
π Themes

Configure a theme with --ui-theme, KAT_UI_THEME, or via config:
ui:
theme: "dracula"
You can optionally set different themes for different profiles:
profiles:
helm:
ui:
theme: "dracula"
# ...
ks:
ui:
theme: "tokyonight-storm"
# ...
We use Chroma for theming, so you can use any styles from the Chroma Style Gallery.
You can also add your own themes in the config:
ui:
theme: "my-custom-theme"
themes:
my-custom-theme:
styles:
background: "#abb2bf bg:#282c34"
punctuation: "#abb2bf"
keyword: "#c678dd"
name: "bold #e06c75"
comment: "italic #8b949e"
commentSpecial: "bold italic #8b949e"
# ...
Chroma uses the same syntax as Pygments. Define ui.themes.[name].styles as a map of Pygments Tokens to Styles. You can then reference any theme in ui.theme (or by using the corresponding flag / env var).