# The programs.kli Module


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:

```nix
{
  inputs.kli.url = "github:kleisli-io/kli";
}
```

Import the module for your system and turn it on. Home Manager:

```nix
{
  imports = [ inputs.kli.homeManagerModules.default ];

  programs.kli.enable = true;
}
```

NixOS:

```nix
{
  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:

```nix
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](#extensions). |
| `settings` | attribute set | `{}` | `settings.json` keys baked as the [lowest settings layer](#settings). |
| `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](#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](#sandbox). |
| `package` | package | image built from the options above | The package to install. Set it to install an image of your own. See [package](#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:

```nix
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](/kli/extend/publishing-extensions#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](/kli/config/settings).

### 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](/kli/concepts/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:

```nix
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](/kli/extend/sharing-extensions).

Bake the extensions you always want into the image, and install the occasional one at runtime.

## Related

- [Distribute as a Nix package](/kli/extend/publishing-extensions#distribute-as-a-nix-package) — the author side: building an extension into a Nix package this module can consume.
- [Sharing extensions](/kli/extend/sharing-extensions) — the runtime install, the imperative alternative to baking in.
- [settings.json](/kli/config/settings) — the keys `programs.kli.settings` bakes.
- [Installation](/kli/cli/installation#nix) — installing kli itself via the flake.
- [Security model and sandboxing](/kli/concepts/security-model-and-sandboxing) — the confinement `sandbox` configures.
