Introduction
Every new dialogue with an assistant is a blank slate. It doesn't remember what we talked about in previous sessions. I wasn't okay with that. I started looking for a solution and settled on OpenClaw: declarative configuration, long-term memory via QMD, Telegram bot, GLM-4.7-flash — all reproducible on any machine with a single command.
Architecture
There is OpenClaw Gateway. Attached to it are Telegram (communication channel), Workspace (files defining the assistant's personality), LLM (the model generating responses), and QMD (long-term memory via vector search). Everything is described in Nix.
Before NixOS, I was on Arch. Updates regularly broke the system: package versions drifted apart, dependencies didn't match. Plus I had to handle rollbacks manually. With NixOS, you configure it and forget it. If something goes wrong, roll back to a commit or boot a previous generation.
Breaking down flake.nix
{
description = "NixOS configuration with Hyprland";
inputs = {
nixpkgs.url = "nixpkgs";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
agenix.url = "github:ryantm/agenix";
agenix.inputs.nixpkgs.follows = "nixpkgs";
nix-openclaw.url = "github:openclaw/nix-openclaw";
openclaw-workspace = {
url = "path:/home/vokrob/.config/openclaw";
flake = false;
};
};
outputs = { self, nixpkgs, home-manager, agenix, nix-openclaw, openclaw-workspace, ... }@inputs: {
nixosConfigurations.vokrob = nixpkgs.lib.nixosSystem {
specialArgs = { inherit nix-openclaw openclaw-workspace; };
system = "x86_64-linux";
modules = [
home-manager.nixosModules.home-manager
agenix.nixosModules.default
./hosts/nixos
];
};
};
}
-
nix-openclaw: flake from
github:openclaw/nix-openclaw. Pulls in Home Manager modules, a package overlay, and a binary cache oncache.garnix.io. Garnix is useful because building OpenClaw doesn't rebuild dependencies from scratch; - openclaw-workspace: just a path, not a flake. A directory with files that define how the assistant behaves;
-
specialArgs: passes external inputs into the Nix module system. Without it, modules won't see
openclaw-workspace.
I only have one host, but if I added a server or a laptop, the configuration would stay the same.
Modular architecture
The configuration is split into two levels.
System modules
Located in modules/nixos/default.nix. This includes base.nix (bootloader, kernel), networking.nix (NetworkManager), services.nix (Hyprland and AmneziaWG for me), security.nix (disabled password for power-off via Polkit), and users.nix (vokrob, agenix, zsh).
In base.nix I add the overlay:
nixpkgs.overlays = [
nix-openclaw.overlays.default
(import ../../overlays)
];
The overlay adds OpenClaw packages to pkgs, including openclaw-gateway.
User modules
Imported via modules/home/default.nix. The main one is features/openclaw.nix.
Plus in hosts/nixos/default.nix I added:
home-manager.sharedModules = [nix-openclaw.homeManagerModules.openclaw];
sharedModules makes the OpenClaw module available in all Home Manager configurations. If a second host appears, the module is already there.
Breaking down openclaw.nix
The main configuration is described in modules/home/features/openclaw.nix.
Workspace
programs.openclaw.workspace.bootstrapFiles = {
agents = "${openclaw-workspace}/AGENTS.md";
soul = "${openclaw-workspace}/SOUL.md";
tools = "${openclaw-workspace}/TOOLS.md";
identity = "${openclaw-workspace}/IDENTITY.md";
user = "${openclaw-workspace}/USER.md";
};
These five files shape the assistant's personality: AGENTS.md defines agent roles and routing, SOUL.md is the base instruction, TOOLS.md describes tools, IDENTITY.md sets communication style, USER.md holds my data.
The files are in ~/.config/openclaw/, and you can change them without rebuilding the system. Just edit SOUL.md, and the assistant will start behaving differently.
Secrets
programs.openclaw.environment = {
ZHIPU_API_KEY = "/run/agenix/openclaw-zhipu-key";
OPENCLAW_GATEWAY_TOKEN = "/run/agenix/openclaw-gateway-token";
};
The values are not the keys themselves, but paths to files. OpenClaw reads secrets from files. Nix substitutes paths at build time, and agenix decrypts secrets during configuration activation and places them in /run/agenix/. They never end up in /nix/store/.
The Telegram token is passed separately:
channels.telegram.tokenFile = "/run/agenix/openclaw-telegram-token";
Integrations
config = {
gateway.mode = "local";
channels.telegram = {
tokenFile = "/run/agenix/openclaw-telegram-token";
allowFrom = [5748618304];
};
};
The local mode means the gateway runs without being tied to the OpenClaw Cloud. Everything is on my machine. The gateway listens on a local port and authorizes requests via a token from /run/agenix/openclaw-gateway-token. Only I have access to the Telegram bot (the allowFrom list).
GLM-4.7-flash
models.providers.openai = {
baseUrl = "https://open.bigmodel.cn/api/paas/v4";
apiKey = {
source = "env";
provider = "default";
id = "ZHIPU_API_KEY";
};
models = [{
name = "glm-4.7-flash";
id = "glm-4.7-flash";
api = "openai-completions";
contextWindow = 200000;
}];
};
The provider is called openai, but baseUrl points to Z.ai. Z.ai provides an OpenAI-compatible endpoint. Through api = "openai-completions", OpenClaw uses the standard OpenAI SDK. The glm-4.7-flash model is free, with a 200K token context window.
QMD — long-term memory
memory.backend = "qmd";
QMD is a Qdrant-based sidecar that doesn't require a separate server.
Each message and response is vectorized, and embeddings go into QMD with metadata. When I write something new, semantic search picks up fragments from the past and mixes them into the prompt. The assistant remembers what we talked about in previous sessions.
I tested it — it remembers:
The "remember" and "forget" commands work through MEMORY.md. QMD just adds semantic search over the entire history on top.
Elevated privileges
tools.elevated = {
enabled = true;
allowFrom = {
telegram = [5748618304];
};
};
This section grants access to dangerous tools via the Telegram bot: executing commands on the host, installing packages, managing processes.
Additional options
agents.defaults = {
model.primary = "openai/glm-4.7-flash";
thinkingDefault = "low";
compaction.reserveTokensFloor = 20000;
};
reloadScript.enable = true;
bundledPlugins = {
summarize.enable = true;
};
-
thinkingDefault: reasoning depth. Set tolow— everyday commands don't need deep thinking; -
compaction.reserveTokensFloor: reserves 20K tokens for memory and system prompt. When I didn't reserve it, the context got eaten up and the assistant started making silly mistakes; -
reloadScript: generates a config reload script without restarting the gateway; -
bundledPlugins.summarize: automatically summarizes URLs and PDFs sent in the chat.
The gateway runs as a systemd user service: it starts when I log in and restarts if it crashes. I check logs via journalctl --user -u openclaw-gateway -f. If something goes wrong, I run systemctl --user restart openclaw-gateway.
Secrets
In secrets/ there are three encrypted files:
-
openclaw-telegram-token.age; -
openclaw-zhipu-key.age; -
openclaw-gateway-token.age.
In the config it looks like this:
age.secrets = {
"openclaw-telegram-token" = {
file = ../../secrets/openclaw-telegram-token.age;
owner = "vokrob";
group = "users";
mode = "0400";
};
};
Agenix encrypts files using age. The key is stored locally at ~/.config/agenix/age-key.txt. Decryption only happens during system activation. Secrets never end up in /nix/store/, so they can't be accidentally committed or exposed in the binary cache. Even if someone gains access to the store, they won't see the API keys.
Message lifecycle
I send a message to the Telegram bot. The Telegram bot sends it to the gateway. The gateway first checks allowFrom — if I'm on the list, it proceeds. Then the gateway queries QMD for context from past conversations. It builds a prompt: system instructions + memory context + my message. It sends this to GLM-4.7-flash. The model's response is saved as a new memory fragment by the gateway and sent back to Telegram.
The chain takes anywhere from a few seconds to several minutes due to the queue for the free model.
Problems
Service won't start
The first thing I ran into: the gateway wouldn't start. The reason: agenix didn't apply the activation script. Fixed by running sudo nixos-rebuild switch, then checking /run/agenix/:
ls -la /run/agenix/
If the files aren't there, there's an error in the age.secrets definitions. In my case, I forgot to specify owner and mode, so agenix didn't create the files.
Telegram bot doesn't respond
Two possible causes: either allowFrom is wrong, or the token is invalid. You can find out your Telegram ID via @userinfobot. I made a mistake: I entered the chat ID instead of the user ID.
To check the token:
journalctl --user -u openclaw-gateway | grep -i telegram
QMD doesn't return context
Right after setup, QMD is empty and the assistant responds as if it has no memory. This is normal. Embeddings accumulate as you interact. You just need to keep talking.
Gateway token mismatch
This can happen if you recreate the token without the gateway knowing about it:
agenix -e secrets/openclaw-gateway-token.age
systemctl --user restart openclaw-gateway
Conclusion
It took an evening, but it was worth it. Now I have a reproducible AI assistant that remembers what we talked about and deploys with a single command. The configuration is identical on any machine: clone the repo, run nixos-rebuild, and you're done.
Code on GitHub. Replace the secrets with your own and build.



Top comments (0)