The programs.kli Module
On this page
kli adds an extension one of two ways. Imperatively, kli install <url> <pin> fetches and verifies one at runtime, per user, with no rebuild. Declaratively, programs.kli compiles a kli image with your extensions and settings baked in, so they are present at every launch. This page is the declarative surface: a Home Manager module and a NixOS module, both from the kli flake. Trust comes from your flake inputs and Nix hashing — there is no per-URL pin and nothing installs at runtime.
Enable the module
Add the kli flake as an input:
{
inputs.kli.url = "github:kleisli-io/kli";
}
Import the module for your system and turn it on. Home Manager:
{
imports = [ inputs.kli.homeManagerModules.default ];
programs.kli.enable = true;
}
NixOS:
{
imports = [ inputs.kli.nixosModules.default ];
programs.kli.enable = true;
}
With enable alone the module installs a plain kli image with no extensions baked in. The Home Manager module installs it into home.packages; the NixOS module into environment.systemPackages.
Bake in an extension
List a flake package in extensions and it is compiled into the image. cairn, for instance, is a flake input whose package you name; it needs SQLite on the library path for its FTS5-backed store:
programs.kli = {
enable = true;
extensions = [ inputs.cairn.packages.${system}.default ];
blessedNativeLibs = [ pkgs.sqlite ];
settings.defaultModel = "claude-opus-4-8";
};
Rebuild and the extension is part of the image. It loads at every launch with no runtime install and no pin, and stays put until you edit the configuration and rebuild.
Options
| Option | Type | Default | Effect |
|---|---|---|---|
enable | boolean |
false | Install a kli image built from these options. |
extensions | list of packages |
[] | Extensions compiled into the image. Each is a Nix package following the extension contract. |
settings | attribute set |
{} |
settings.json keys baked as the lowest settings layer. |
registries | list |
[] | Registry entries baked into the image's configuration. |
blessedNativeLibs | list of packages |
[] | Native libraries placed on the image's library search path. See blessedNativeLibs. |
dataDir | null or string |
null |
Project-local data directory. null resolves it from the runtime environment. |
sandbox | null or submodule |
null |
Whole-process bwrap confinement. null leaves the binary unwrapped. See sandbox. |
package | package | image built from the options above | The package to install. Set it to install an image of your own. See package. |
extensions
An extension you list here is a Nix package, not a URL. kli composes it into the image as a build dependency, so it must be a buildLisp library — the builder kli itself is built with — carrying two passthru fields:
passthru.name— the extension id.passthru.manifestSymbol— the package-qualified symbol naming the extension's manifest. The build fails if the compiled image does not export it, so a mislabelled package cannot ship silently.
The list must not include the core kli library; the module asserts against it. As a consumer you take the author's flake as an input and add its package:
programs.kli.extensions = [ inputs.some-extension.packages.${system}.default ];
Producing such a package — the buildLisp library, where buildLisp comes from, and the passthru contract — is the author side, covered in Distribute as a Nix package.
settings
settings bakes the same settings.json keys the on-disk files use, as the lowest-precedence layer. The merge order is baked < global < project, with the active profile overlay applied last, so a user's ~/.config/kli/settings.json always overrides what you bake. Use it to ship image defaults — a default model, theme, or capability set — that a user can still override without editing your configuration. The keys and their merge rules are in settings.json.
blessedNativeLibs
Native shared libraries added to the image's library search path. An extension that binds a C library names it here so the symbol resolves at load: cairn needs pkgs.sqlite for its FTS5 store. Extensions with no native dependency leave this empty.
sandbox
Whole-process confinement via bwrap — confinement the binary cannot switch off. null leaves the binary unwrapped. An attribute set wraps the entrypoint so the working directory is the writable root and the rest of the filesystem is read-only, with the process and IPC namespaces unshared.
| Field | Type | Default | Effect |
|---|---|---|---|
network | boolean |
true |
Keep network access. false unshares the network namespace, which also cuts the model API — usable only with a local model. |
writablePaths | list of strings |
[] | Extra paths bound writable, beyond the working directory. |
denyRead | list of strings |
[] | Paths hidden from the confined process: a file reads as empty, a directory as empty. The at-rest mask for secrets such as credential files. |
denyEnv | list of strings |
[] |
Environment variable names unset before the process starts. The in-environment twin of denyRead. |
nestedUserns | boolean |
false |
Set when the image runs inside an outer unprivileged user namespace whose /proc is masked, such as a systemd-nspawn or OCI container. |
The sandbox model, and how it relates to the capability layer, is in Security model and sandboxing.
package
The module builds package from the options above by default. Set it to install an image you built yourself — with the flake's lib.<system>.mkConfiguredKli (below) or any override — and the other build options no longer describe what gets installed.
Without a module
Outside a Home Manager or NixOS configuration, the flake exposes lib.<system>.mkConfiguredKli. It takes the same options and returns the image derivation, for a nix build target or a devShell:
inputs.kli.lib.${system}.mkConfiguredKli {
extensions = [ inputs.cairn.packages.${system}.default ];
blessedNativeLibs = [ pkgs.sqlite ];
}
The module's package default is exactly this call over your options.
Declarative or imperative
The two channels coexist, and you can use both in one setup:
- Declarative — this module. Extensions are compiled into the image at build time, reproducible, trusted through your flake inputs and Nix hashing, present at every launch. Preferred for Nix users.
- Imperative —
kli install <url> <pin>at runtime, per user, verified by a git-object pin and an opt-in signature, no rebuild. See Sharing extensions.
Bake the extensions you always want into the image, and install the occasional one at runtime.
Related
- Distribute as a Nix package — the author side: building an extension into a Nix package this module can consume.
- Sharing extensions — the runtime install, the imperative alternative to baking in.
- settings.json — the keys
programs.kli.settingsbakes. - Installation — installing kli itself via the flake.
- Security model and sandboxing — the confinement
sandboxconfigures.