# Install the ChatGPT and Codex Desktop App Declaratively on NixOS

Install the unified ChatGPT and Codex desktop application declaratively on NixOS with a pinned llm-agents.nix flake input.

Author: Ruhani Rabin
Canonical: https://rabin.blog/install-chatgpt-codex-desktop-app-declaratively-nixos/
Published: 2026-09-15T06:33:13.000Z
Updated: 2026-09-20T18:21:59.000Z

I wanted to use the ChatGPT/Codex desktop application natively, which worked on NixOS with one command:

```bash
NIXPKGS_ALLOW_UNFREE=1 nix run --impure github:numtide/llm-agents.nix#chatgpt
```

That was temporary testing, but it was not how I wanted to keep it.

My workstation is configured declaratively. If an application becomes part of my normal workflow, I want to put it in the **NixOS** configuration - not hidden inside terminal history as a command, which would be difficult to remember later on.

The target was simple:

* install the unified ChatGPT and Codex desktop application;
* keep its version pinned through `flake.lock`;
* install only this package, not every agent in the collection;
* avoid unnecessarily trusting another binary cache;
* keep login credentials and application state outside Git.

This is the configuration I ended up using, as of now.

You might also want to know how to get [Affinity Photos, Designer and Publisher v2 working with NixOS](https://rabin.blog/run-affinity-v2-on-nixos/).

## The package source

The package comes from [`numtide/llm-agents.nix`](https://github.com/numtide/llm-agents.nix).

Even though the name is as you can see above, adding this repository as a flake input does not install every LLM agent it contains. It only makes its package outputs available to the Nix configuration.

The package selected in this guide is:

```nix
llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.chatgpt
```

That installs the unified ChatGPT and Codex desktop application. Claude Code, Gemini CLI, OpenCode, and the other packages in the collection are not installed unless they are explicitly added.

**Warning**: This is third-party Nix packaging of the application. It is not an OpenAI-maintained Nix flake.

## Add the flake input

My configuration already uses flakes, so I added the new input to `flake.nix`:

flake.nix

```nix
inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";


  home-manager = {
    url = "github:nix-community/home-manager/release-26.05";
    inputs.nixpkgs.follows = "nixpkgs";
  };


  llm-agents.url = "github:numtide/llm-agents.nix";
};
```

I deliberately did **not** add this:

```nix
llm-agents.inputs.nixpkgs.follows = "nixpkgs";
```

The `llm-agents.nix` project is built and tested against its own pinned `nixpkgs-unstable` input. My workstation follows the stable `nixos-26.05` branch.

Forcing the external flake to use my stable Nixpkgs might look cleaner because it removes a second Nixpkgs input, but upstream specifically warns that *this combination can eventually break*.

Keeping the upstream pin costs a little more lock-file space. In exchange, I get the dependency combination the package was actually tested with.

That is a reasonable trade-off for a desktop application I want to keep working.

## Pass the input to the NixOS modules

My flake passes external package inputs through `specialArgs`.

The relevant part looks like this:

flake.nix

```nix
outputs = {
  nixpkgs,
  home-manager,
  llm-agents,
  ...
}:
{
  nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
    system = "x86_64-linux";


    specialArgs = {
      inherit llm-agents;
    };


    modules = [
      ./configuration.nix
    ];
  };
};
```

If your configuration already passes the complete `inputs` set instead, you can use that structure. There is no reason to reorganize a working flake only for this package.

## Add only the ChatGPT package

My normal application packages live in `packages.nix`, so I added `llm-agents` to the module arguments:

packages.nix

```nix
{ pkgs, llm-agents, ... }:
```

Then I added the package to `environment.systemPackages`:

packages.nix

```nix
{
  environment.systemPackages = with pkgs; [
    # Other applications


    # Unified ChatGPT and Codex desktop application
    llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.chatgpt
  ];
}
```

Only that output becomes part of the system closure.

The large upstream package collection is available through the flake, but Nix does not install everything merely because the input exists.

## Allow the unfree package

The application is distributed under an **unfree** license, so Nix must be configured to allow it:

```nix
nixpkgs.config.allowUnfree = true;
```

I already had this enabled previously, for other desktop packages.

Once it is part of the NixOS configuration, I do not need to keep launching it with:

```bash
NIXPKGS_ALLOW_UNFREE=1
```

That environment variable was only useful for the initial non-persistent test.

## Update the lock file

After editing `flake.nix`, I ran:

```bash
nix flake lock
```

The resulting change was larger than one package entry because the lock file also records the external flake’s transitive inputs, including its Nixpkgs revision and build tooling.

That still **does not mean every agent was installed**. The lock file records inputs; the package list decides what enters the system.

The important part is that the exact upstream revision is now pinned. A future rebuild uses the same package source until I intentionally update it.

## What were all the trust prompts?

During the first `nix run`, Nix asked about these settings:

```text
allow-import-from-derivation
extra-substituters
extra-trusted-public-keys
```

The last two referred to Numtide’s optional binary cache:

```text
https://cache.numtide.com
```

Accepting them would allow Nix to download artifacts signed by that cache. This can make builds faster, but it also expands the set of binary caches trusted by the machine.

**Tip**: I chose the conservative path and did not add that cache or its signing key to my system configuration.

It was not required for the application to work.

I also did not use:

```bash
--accept-flake-config
```

The remote flake may propose settings when it is run directly, but I do not need to copy all of those settings into my NixOS configuration merely to consume one package output.

## Build and verify

Before switching the running system, I checked the flake:

```bash
nix flake check --no-build
```

Then I evaluated and built the NixOS configuration using my normal workflow.

For a conventional configuration, that would be:

```bash
sudo nixos-rebuild test --flake .#nixos
sudo nixos-rebuild switch --flake .#nixos
```

After the switch, the application can be launched from the desktop application launcher as **ChatGPT**, or from a terminal:

```bash
chatgpt
```

Both ChatGPT and Codex are available through the unified application. And, system tray application icon is available on Niri+DMS.

## What about updating it later

Because the source is pinned in `flake.lock`, it will not silently change during an ordinary rebuild.

When I want a newer version, I can update only by using this:

```bash
nix flake update llm-agents
```

Then I review the lock-file change and test the system again.

I avoid updating every flake input at the same time unless that is intentional. A focused update makes it much easier to identify what caused a regression.

## Final configuration

The essential configuration is small:

flake.nix

```nix
{
  inputs.llm-agents.url = "github:numtide/llm-agents.nix";


  outputs = { nixpkgs, llm-agents, ... }: {
    nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";


      specialArgs = {
        inherit llm-agents;
      };


      modules = [
        ./configuration.nix
      ];
    };
  };
}
```

And inside the package module:

packages.nix

```nix
{ pkgs, llm-agents, ... }:


{
  nixpkgs.config.allowUnfree = true;


  environment.systemPackages = [
    llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.chatgpt
  ];
}
```

The final result is exactly how I wanted it:

```mermaid
flowchart TB
    A["🔒 flake.lock"]
    B["📦 Pinned llm-agents.nix"]
    C{"Select a package output"}
    D["✅ chatgpt"]
    E["Other agents remain available<br/>but are not installed"]
    F["💬 ChatGPT + Codex<br/>desktop application"]


    A --> B --> C
    C --> D --> F
    C -. not selected .-> E
```

No separate install script. No accidental installation of the entire agent collection.

It is simply another declared part of the workstation, which is repeatable in other machines.
