Introduction
Decknix is an opinionated Nix framework for macOS configuration management. It combines Nix Flakes, nix-darwin, and home-manager into a batteries-included system that's easy to customise and share across teams.
Why Decknix?
- One command to set up a Mac — bootstrap installs Nix, nix-darwin, and your full dev environment
- Layered configuration — framework defaults → org/team configs → personal overrides
- Everything in Nix — editors, shell, git, window manager, CLI tools, AI tooling
- Team-friendly — org configs are versioned flake inputs that everyone shares
- Easy to override — every default uses
lib.mkDefault, so your preferences always win
What's Included
| Category | Highlights |
|---|---|
| Editors | Emacs (full IDE with 13+ modules), Vim |
| Shell | Zsh with Starship prompt, completions, syntax highlighting |
| Git | Delta diffs, Magit, Forge (GitHub PRs from Emacs) |
| Dev Tools | ripgrep, jq, curl, gh CLI, language servers |
| Window Manager | AeroSpace tiling WM with fuzzy workspace picker |
| AI Tooling | Augment Code agent with declarative MCP config |
| CLI | decknix switch, decknix update, extensible subcommands |
How This Documentation Is Organised
- Getting Started — install and build your first configuration
- Architecture — understand the 3-layer model, config loader, and directory layout
- Configuration — customise settings, features, secrets, and org configs
- Modules — explore every module: editors, shell, git, WM, AI
- CLI Reference — core commands and the extension system
- Guides — set up org configs for your team, develop the framework, troubleshoot
Installation
This guide walks you through setting up decknix on a fresh or existing macOS system.
Prerequisites
- macOS (Apple Silicon or Intel)
- Administrator access (for initial Nix installation)
- ~10GB disk space for the Nix store
Option 1: Fresh Install (Recommended)
Run the bootstrap script to install everything from scratch:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/ldeck/decknix/main/bin/bootstrap)"
This will:
- Install Nix with flakes enabled
- Install nix-darwin
- Create your local config directory at
~/.config/decknix/ - Initialize a flake in
~/.config/decknix/ - Prompt you for username and hostname
Option 2: Existing Nix Installation
If you already have Nix with flakes enabled:
# Create and enter your config directory
mkdir -p ~/.config/decknix && cd ~/.config/decknix
# Initialize from template
nix flake init -t github:ldeck/decknix
# Edit your settings
$EDITOR settings.nix
Edit settings.nix with your machine details:
{
username = "your-username"; # macOS username
hostname = "your-hostname"; # Machine name
system = "aarch64-darwin"; # or "x86_64-darwin" for Intel
role = "developer"; # "developer", "designer", or "minimal"
}
Then build and switch:
decknix switch
# Or if decknix CLI isn't installed yet:
sudo darwin-rebuild switch --flake .#default --impure
For Organisation Members
If your team maintains an org config repo, check their README for a dedicated bootstrap script that sets up both decknix and the team configuration in one step.
See Organisation Configs for how org configs work.
Next Steps
- First Configuration — set up your identity and packages
- Applying Changes — learn the day-to-day workflow
First Configuration
After installation, you'll have this directory structure:
~/.config/decknix/
├── flake.nix # Main flake (imports decknix)
├── flake.lock # Locked dependencies
├── settings.nix # username, hostname, system, role
└── local/ # Your personal overrides
├── home.nix # Home-manager config
└── system.nix # Darwin system config
Set Up Git Identity
Edit ~/.config/decknix/local/home.nix:
{ pkgs, ... }: {
programs.git.settings = {
user.email = "you@example.com";
user.name = "Your Name";
};
}
Add Your Packages
{ pkgs, ... }: {
home.packages = with pkgs; [
nodejs
python3
go
];
}
Choose Your Editor Profile
Decknix offers tiered editor profiles:
| Profile | Emacs Includes | Vim Includes |
|---|---|---|
minimal | Core, completion, editing, UI, undo | Base config |
standard | + development, magit, treemacs, languages, welcome | + whitespace, skim |
full (default) | + LSP, org-mode, HTTP client, agent-shell | — |
custom | Your own config (disables framework) | Your own config |
Change profiles in your home.nix:
{ ... }: {
decknix.editors.emacs.profile = "standard";
decknix.editors.vim.profile = "minimal";
}
Apply Your Changes
decknix switch
The first build takes a few minutes as it downloads packages. Subsequent builds are faster.
Verify Installation
# Check Emacs daemon is running
launchctl list | grep emacs
# Open a file in Emacs
ec test.txt
# Check Magit
# In Emacs: C-x g (opens git status)
Next Steps
- Applying Changes — day-to-day workflow
- Personal Overrides — directory layout and advanced customisation
- Secrets & Authentication — GitHub tokens, GPG, SSH keys
Applying Changes
Day-to-Day Workflow
The core loop is: edit → switch → done.
Switch to Your New Configuration
decknix switch
This runs darwin-rebuild switch under the hood, applying both system and home-manager changes atomically.
Dry Run (Build Without Activating)
decknix switch --dry-run
Builds the configuration to check for errors without actually switching to it.
Update Framework and Dependencies
# Update all flake inputs (decknix, nixpkgs, etc.)
decknix update
# Update a specific input
decknix update decknix
After updating, run decknix switch to apply.
Test Local Framework or Org-Config Changes
Point decknix switch at a local checkout of any flake input using
--override INPUT=PATH (repeatable):
# Test a local decknix checkout
decknix switch --override decknix=~/tools/decknix
# Test decknix and your org-config together
decknix switch \
--override decknix=~/tools/decknix \
--override nc-config=~/Code/my-org/decknix-config
Each --override becomes --override-input <INPUT> path:<PATH> on the
underlying darwin-rebuild call.
If you use the same overrides every day, pin them in
~/.config/decknix/settings.toml so plain decknix switch picks them up
automatically — see decknix switch → Persistent overrides.
Common Patterns
Edit Personal Config
$EDITOR ~/.config/decknix/local/home.nix
decknix switch
Add a New Package
# ~/.config/decknix/local/home.nix
{ pkgs, ... }: {
home.packages = with pkgs; [
kubectl
helm
];
}
decknix switch
Disable a Module
# ~/.config/decknix/local/home.nix
{ ... }: {
programs.emacs.decknix.welcome.enable = false;
decknix.wm.aerospace.enable = false;
}
decknix switch
Troubleshooting
"command not found: decknix"
The CLI isn't in your path yet. Use the full command:
sudo darwin-rebuild switch --flake ~/.config/decknix#default --impure
Build Errors
Check the trace output to see which files were loaded:
[Loader] home + /Users/you/.config/decknix/local/home.nix
[Loader] system + /Users/you/.config/decknix/local/system.nix
Reset to Clean State
rm -rf ~/.config/decknix
# Re-run bootstrap or nix flake init
How Decknix Works
Decknix uses a 3-layer configuration model where each layer can override the one below it.
The 3 Layers
┌─────────────────────────────────────────┐
│ Layer 3: Personal Overrides │ ~/.config/decknix/local/
│ Your packages, identity, preferences │ ~/.config/decknix/<org>/
├─────────────────────────────────────────┤
│ Layer 2: Organisation Configs │ Flake inputs (versioned repos)
│ Team tools, standards, shared settings │ e.g. inputs.my-org-config
├─────────────────────────────────────────┤
│ Layer 1: Decknix Framework │ github:ldeck/decknix
│ Sensible defaults for everything │ darwinModules + homeModules
└─────────────────────────────────────────┘
Layer 1 — Framework
The decknix flake provides darwinModules.default and homeModules.default containing opinionated defaults for shell, editors, git, window management, and more. Every value uses lib.mkDefault, so it can be overridden by any higher layer without lib.mkForce.
Layer 2 — Organisation Configs
Teams create separate repos (e.g. github:MyOrg/decknix-config) that export their own darwinModules.default and homeModules.default. These are added as flake inputs and supply team-specific tools, packages, and settings.
Layer 3 — Personal Overrides
Each user's ~/.config/decknix/ directory contains personal overrides that are auto-discovered and merged at build time. These live outside git (or in a personal dotfiles repo) and let you customise without touching shared configs.
How Builds Work
When you run decknix switch:
mkSystemreads yoursettings.nix(username, hostname, system, role)- Constructs a
darwinConfigurations.defaultthat merges:- Framework modules (Layer 1)
- Org modules passed via
darwinModules/homeModules(Layer 2) configLoaderoutput from~/.config/decknix/(Layer 3)
- Calls
darwin-rebuild switchto atomically activate the new generation
Key Design Decisions
lib.mkDefaulteverywhere — the framework never fights your preferences- Filesystem auto-discovery — drop a
.nixfile in the right place and it's loaded - Flake inputs for teams — version-pinned, reproducible, Renovate-watchable
- Secrets separated —
secrets.nixfiles are gitignored and loaded alongsidehome.nix - Impure builds —
--impureis required so the config loader can read~/.config/decknix/at build time
Next
- Directory Layout — where everything lives
- Config Loader — how files are discovered and merged
Directory Layout
User Configuration
~/.config/decknix/ # Your flake + personal overrides
├── flake.nix # Main flake (imports decknix + org configs)
├── flake.lock # Pinned dependency versions
├── settings.nix # username, hostname, system, role
│
├── local/ # Personal overrides (always loaded)
│ ├── home.nix # Packages, git identity, shell aliases
│ ├── system.nix # macOS system preferences
│ └── secrets.nix # Auth tokens, keys (gitignored)
│
├── <org-name>/ # Per-org personal overrides
│ ├── home.nix # Org-specific personal tweaks
│ ├── system.nix
│ ├── secrets.nix
│ └── home/ # Nested home modules (recursively loaded)
│ └── extra.nix
│
└── secrets.nix # Root-level secrets (also supported)
Key Points
local/is for generic personal config — git identity, extra packages, shell aliases<org-name>/directories match flake input names — overrides specific to that orgsecrets.nixfiles are gitignored and loaded alongsidehome.nixhome/subdirectories are recursively scanned for additional.nixfiles- All directories are auto-discovered — no registration needed
Framework Source
decknix/
├── bin/ # Bootstrap scripts
│ └── bootstrap.sh # Fresh install script
├── cli/ # Rust CLI source
│ └── src/main.rs # switch, update, help, extensions
├── docs/ # This documentation site
├── lib/
│ ├── default.nix # mkSystem + configLoader
│ └── find.nix # File discovery utilities
├── modules/
│ ├── cli/ # decknix CLI nix-darwin module
│ │ └── default.nix # Subtask system, extensions.json
│ ├── common/
│ │ └── unfree.nix # Unfree package allowlist
│ ├── darwin/ # macOS system modules
│ │ ├── default.nix # System packages, fonts, defaults
│ │ ├── aerospace.nix # AeroSpace tiling WM (system-level)
│ │ └── emacs.nix # Emacs daemon service
│ └── home/ # Home-manager modules
│ ├── default.nix # Imports + default packages
│ ├── options.nix # Role templates, core options
│ └── options/
│ ├── cli/ # auggie, board, extensions, nix-github-auth
│ ├── editors/ # emacs/ (13 modules), vim/
│ └── wm/ # aerospace/, hammerspoon/, spaces.nix
├── pkgs/ # Custom Nix packages
├── templates/ # Flake templates for `nix flake init`
└── flake.nix # Framework flake
Config Loader
The config loader (decknix.lib.configLoader) is the engine that discovers and merges personal override files from ~/.config/decknix/.
How It Works
- Scan
~/.config/decknix/for all subdirectories - For each directory, look for:
identity.nix— org user identity (auto-wired toconfig.<org>.user.*)home.nix— home-manager modulesystem.nix— nix-darwin modulesecrets.nix— secrets (merged into home-manager)home/**/*.nix— recursively loaded home modules
- Also check for root-level files (
~/.config/decknix/home.nix, etc.) - Import every discovered file and trace what was loaded
Discovery Order
~/.config/decknix/
├── local/home.nix ← loaded
├── local/system.nix ← loaded
├── local/secrets.nix ← loaded (merged into home)
├── nurturecloud/identity.nix ← auto-wired to config.nurturecloud.user.*
├── nurturecloud/home.nix ← loaded (can reference config.nurturecloud.user.*)
├── nurturecloud/system.nix ← loaded
├── secrets.nix ← loaded (root-level)
└── home.nix ← loaded (root-level)
All files are merged — ordering within a layer is discovery order (alphabetical by directory name).
Identity Files
When <org>/identity.nix exists, the loader auto-generates NixOS module options under config.<org>.user.* and injects them into both darwin and home-manager module systems. This means any Nix module — org configs, personal overrides, or framework modules — can reference the identity without imports.
Creating an identity file
The file is a plain Nix attrset (not a module):
# ~/.config/decknix/nurturecloud/identity.nix
{
email = "you@nurturecloud.com";
name = "Your Name";
githubUser = "your-github";
gpgKey = "ABCDEF1234567890"; # optional — omit or leave empty
}
Using identity in modules
The directory name becomes the option namespace. For a directory named nurturecloud, the following options are available everywhere:
{ config, ... }: {
# In any darwin or home-manager module:
some.service.email = config.nurturecloud.user.email;
some.service.name = config.nurturecloud.user.name;
# etc.
}
Available options
| Option | Type | Description |
|---|---|---|
config.<org>.user.email | str | User email for the organisation |
config.<org>.user.name | str | User full name |
config.<org>.user.githubUser | str | GitHub username |
config.<org>.user.gpgKey | str | GPG signing key ID (empty if not set) |
Multi-org support
Each org directory can have its own identity.nix with different values. A user working across two orgs might have:
~/.config/decknix/
├── nurturecloud/identity.nix → config.nurturecloud.user.email = "me@nc.com"
└── sideproject/identity.nix → config.sideproject.user.email = "me@sp.com"
The local/ directory is excluded from identity discovery — it's for personal overrides only.
Trace Output
When you build, the loader traces what it finds:
[Loader] identity + /Users/you/.config/decknix/nurturecloud/identity.nix
[Loader] system + /Users/you/.config/decknix/local/system.nix
[Loader] home + /Users/you/.config/decknix/local/home.nix
[Loader] No secrets modules found.
Use this to verify which files are being picked up:
decknix switch 2>&1 | grep "\[Loader\]"
API Reference
configLoader
decknix.lib.configLoader {
lib = nixpkgs.lib; # Required
username = "your-username"; # Required
hostname = "your-hostname"; # Optional (default: "unknown")
system = "aarch64-darwin"; # Optional (default: "unknown")
role = "developer"; # Optional (default: "developer")
homeDir = "/Users/you"; # Optional (auto-derived from username + system)
configDir = "/Users/you/.config/decknix"; # Optional (auto-derived)
}
Returns:
{
modules = {
home = [ ... ]; # List of imported home + secrets modules
system = [ ... ]; # List of imported system modules
identity = [ ... ]; # Auto-generated identity modules (config.<org>.user.*)
};
allDirs = [ "local" "nurturecloud" ]; # Discovered directory names
}
mkSystem
The top-level builder that wires everything together:
decknix.lib.mkSystem {
inputs; # Your flake inputs (must include decknix, nixpkgs, nix-darwin)
settings = import ./settings.nix; # { username, hostname, system, role }
darwinModules = [ ... ]; # Extra darwin modules (org configs)
homeModules = [ ... ]; # Extra home-manager modules (org configs)
extraSpecialArgs = {}; # Additional args passed to modules
stateVersion = "24.05"; # home.stateVersion
}
Returns: { darwinConfigurations.default = ...; }
The build merges modules in this order:
- Framework
darwinModules.default/homeModules.default - Your
darwinModules/homeModulesargs (org configs) configLoaderidentity modules (config..user.*) configLoadersystem/home modules (personal overrides from filesystem)
Settings Reference
The settings.nix file in your flake directory defines your machine identity:
# ~/.config/decknix/settings.nix
{
username = "ldeck"; # Your macOS username
hostname = "lds-mbp"; # Machine hostname
system = "aarch64-darwin"; # "aarch64-darwin" (Apple Silicon) or "x86_64-darwin" (Intel)
role = "developer"; # Bootstrap template: "developer", "designer", or "minimal"
}
Fields
| Field | Type | Default | Description |
|---|---|---|---|
username | string | "setup-required" | Your macOS login username (whoami) |
hostname | string | "setup-required" | Machine hostname (hostname -s) |
system | string | "aarch64-darwin" | Nix system identifier |
role | enum | "developer" | Determines which bootstrap template is applied |
Roles
The role field selects a starter template for first-time setup:
| Role | What It Adds |
|---|---|
developer | Git config template + nodejs |
designer | Inkscape |
minimal | Nothing extra — blank slate |
After the first build, the role has minimal impact. You can always add or remove packages in your home.nix regardless of role.
Where Settings Are Used
Settings flow into the build via mkSystem:
# flake.nix
outputs = inputs@{ decknix, ... }:
decknix.lib.mkSystem {
inherit inputs;
settings = import ./settings.nix;
};
mkSystem uses them to:
- Set
networking.hostName - Set
system.primaryUser - Derive the home directory path
- Pass
roleto home-manager for template selection - Configure
configLoaderpaths
Personal Overrides
Personal overrides live in ~/.config/decknix/ and are auto-discovered by the config loader.
Directory Structure
~/.config/decknix/
├── local/ # Generic personal config (always loaded)
│ ├── home.nix
│ ├── system.nix
│ └── secrets.nix # Gitignored
├── my-org/ # Per-org overrides (matches flake input name)
│ ├── home.nix
│ └── home/
│ └── kubernetes.nix # Recursively loaded
└── secrets.nix # Root-level secrets
How Overrides Work
Decknix framework defaults all use lib.mkDefault, so any value you set in your personal files wins automatically:
# Framework sets: programs.git.settings.pull.rebase = lib.mkDefault true;
# Your override: programs.git.settings.pull.rebase = false; ← wins
For cases where another module explicitly sets a value (without mkDefault), use lib.mkForce:
{ lib, ... }: {
programs.emacs.decknix.welcome.enable = lib.mkForce false;
}
Common Override Patterns
Add Packages
# ~/.config/decknix/local/home.nix
{ pkgs, ... }: {
home.packages = with pkgs; [
kubectl
helm
terraform
awscli2
];
}
Shell Aliases
{ ... }: {
programs.zsh.shellAliases = {
k = "kubectl";
tf = "terraform";
};
}
Environment Variables
{ ... }: {
home.sessionVariables = {
EDITOR = "emacsclient -c";
AWS_PROFILE = "default";
};
}
macOS System Preferences
# ~/.config/decknix/local/system.nix
{ ... }: {
system.defaults = {
dock.autohide = true;
finder.ShowPathbar = true;
NSGlobalDomain.KeyRepeat = 2;
};
}
Homebrew Casks
# ~/.config/decknix/local/system.nix
{ ... }: {
homebrew.casks = [
"docker"
"slack"
"1password"
];
}
Debugging
See which files are loaded:
decknix switch 2>&1 | grep "\[Loader\]"
Evaluate a specific option without building:
nix repl
:lf .
darwinConfigurations.default.config.home-manager.users.YOU.home.packages
Organisation Configs
Org configs let teams share a standard set of tools, packages, and settings through versioned flake inputs.
How It Works
An org config is a separate git repo that exports darwinModules.default and homeModules.default. These are wired into your flake as inputs:
# ~/.config/decknix/flake.nix
{
inputs = {
decknix.url = "github:ldeck/decknix";
nixpkgs.follows = "decknix/nixpkgs";
nix-darwin.follows = "decknix/nix-darwin";
# Team config
my-org-config = {
url = "github:MyOrg/decknix-config";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs@{ decknix, ... }:
decknix.lib.mkSystem {
inherit inputs;
settings = import ./settings.nix;
darwinModules = [ inputs.my-org-config.darwinModules.default ];
homeModules = [ inputs.my-org-config.homeModules.default ];
};
}
Creating an Org Config Repo
Minimal structure:
my-org-config/
├── flake.nix
├── home.nix # Team home-manager modules
├── system.nix # Team darwin modules
└── README.md
flake.nix
{
description = "My Org - Decknix Config";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs, ... }: {
darwinModules.default = import ./system.nix;
homeModules.default = import ./home.nix;
};
}
home.nix
{ pkgs, ... }: {
home.packages = with pkgs; [
awscli2
terraform
jdk17
];
}
Per-User Identity
Each user creates an identity.nix in their org's config directory. The framework's config loader auto-discovers it and generates config.<org>.user.* options available in all Nix modules:
# ~/.config/decknix/my-org/identity.nix
{
email = "you@my-org.com";
name = "Your Name";
githubUser = "your-github";
gpgKey = "ABCDEF1234567890"; # optional
}
Org modules can then reference the identity without any imports:
# In org config system.nix:
{ config, lib, ... }: {
decknix.services.hub.jira.email = lib.mkDefault config.my-org.user.email;
programs.git.includes = [{
condition = "gitdir:~/Code/my-org/";
contents.user.email = config.my-org.user.email;
}];
}
Org bootstraps should prompt for this identity on first setup and write identity.nix automatically. See Config Loader — Identity Files for full details.
Benefits
- Version pinning —
flake.lockpins a known-good version - Reproducibility — every team member gets the same tools
- Easy updates —
nix flake update my-org-configto pull latest - Automated updates — Renovate or Dependabot can watch for new versions
- Personal overrides — users can still override anything in
~/.config/decknix/<org-name>/ - Identity wiring — per-user org identity flows automatically via
identity.nix
Testing Changes
Before merging changes to an org config, test locally:
cd ~/.config/decknix
decknix switch --override my-org-config=~/Code/my-org/decknix-config
# Or:
nix build .#darwinConfigurations.default.system --impure \
--override-input my-org-config path:~/Code/my-org/decknix-config
See also: Adding Org Configs for Your Team for a step-by-step walkthrough.
Secrets & Authentication
Decknix keeps secrets separate from your main configuration via gitignored secrets.nix files.
How secrets.nix Works
The config loader discovers and loads secrets.nix files alongside home.nix:
~/.config/decknix/secrets.nix(root level)~/.config/decknix/<org>/secrets.nix(per-org)
Both are merged into your home-manager configuration.
Quick Setup
# ~/.config/decknix/local/secrets.nix
{ ... }: {
home.file.".authinfo".text = ''
machine api.github.com login YOUR_USERNAME^forge password ghp_YOUR_TOKEN
'';
}
Make sure secrets.nix is gitignored:
echo "secrets.nix" >> ~/.config/decknix/.gitignore
GitHub Token for Forge
Forge (GitHub PRs in Emacs) needs a Personal Access Token.
1. Create a Token
- Go to GitHub → Settings → Developer Settings → Personal Access Tokens
- Generate a classic token with scopes:
repo,read:org,read:user - Copy the token (starts with
ghp_)
2. Add to secrets.nix
{ ... }: {
home.file.".authinfo".text = ''
machine api.github.com login YOUR_USERNAME^forge password ghp_xxxxxxxxxxxx
'';
}
3. Verify in Emacs
M-x auth-source-search RET
host: api.github.com
user: YOUR_USERNAME^forge
GPG-Encrypted Alternative
# Create and encrypt
echo "machine api.github.com login USER^forge password ghp_xxx" | \
gpg --encrypt --recipient YOUR_KEY_ID > ~/.authinfo.gpg
{ ... }: {
programs.emacs.extraConfig = ''
(setq auth-sources '("~/.authinfo.gpg"))
'';
}
macOS Keychain
{ ... }: {
programs.emacs.extraConfig = ''
(setq auth-sources '(macos-keychain-internet macos-keychain-generic))
'';
}
security add-internet-password -a "USER^forge" -s "api.github.com" -w "ghp_xxx"
Multi-Account GitHub Setup
For multiple GitHub accounts (personal + work), add entries for each:
{ ... }: {
home.file.".authinfo".text = ''
machine api.github.com login personal-user^forge password ghp_personal_xxx
machine api.github.com login work-user^forge password ghp_work_yyy
'';
}
When you first use Forge in a repo, it prompts for which username to use. The choice is stored in .git/config.
Combine with Git conditional includes for automatic email switching:
# ~/.config/decknix/my-org/home.nix
{ ... }: {
programs.git.includes = [{
condition = "gitdir:~/Code/my-org/";
contents.user.email = "you@my-org.com";
}];
}
SSH Keys
{ ... }: {
programs.ssh = {
enable = true;
matchBlocks."github.com" = {
identityFile = "~/.ssh/id_ed25519";
user = "git";
};
extraConfig = ''
AddKeysToAgent yes
UseKeychain yes
'';
};
}
GPG Setup
{ pkgs, ... }: {
home.packages = [ pkgs.gnupg pkgs.pinentry_mac ];
programs.gpg.enable = true;
home.file.".gnupg/gpg-agent.conf".text = ''
pinentry-program ${pkgs.pinentry_mac}/bin/pinentry-mac
default-cache-ttl 3600
max-cache-ttl 86400
'';
}
Nix GitHub Auth
Decknix automatically provides authenticated GitHub API access to Nix (5,000 req/hr instead of 60). This uses gh auth token to generate ~/.config/nix/access-tokens.conf on every decknix switch.
No configuration needed — enabled by default via decknix.nix.githubAuth.enable.
Security Best Practices
- Never commit secrets — always gitignore
secrets.nix - Use GPG encryption — encrypt
.authinfoas.authinfo.gpg - Use short-lived tokens — set token expiration when possible
- Limit token scopes — only grant necessary permissions
- Prefer SSH — use SSH over HTTPS for git operations
- Rotate regularly — update tokens periodically
Enabling & Disabling Features
Every decknix default uses lib.mkDefault, making it easy to override in your personal config.
Emacs Modules
# ~/.config/decknix/local/home.nix
{ ... }: {
# Disable specific modules
programs.emacs.decknix.welcome.enable = false;
programs.emacs.decknix.org.presentation.enable = false;
programs.emacs.decknix.http.enable = false;
# Disable specific language modes
programs.emacs.decknix.languages.rust.enable = false;
programs.emacs.decknix.languages.go.enable = false;
# Disable all Emacs config (use your own)
programs.emacs.decknix.enable = false;
}
See Emacs module reference for all available options.
Editor Profiles
Switch between pre-defined tiers instead of toggling individual modules:
{ ... }: {
decknix.editors.emacs.profile = "standard"; # minimal | standard | full | custom
decknix.editors.vim.profile = "minimal"; # minimal | standard | custom
}
Window Manager
{ ... }: {
# AeroSpace
decknix.wm.aerospace.enable = false;
# Or enable with custom workspaces
decknix.wm.aerospace = {
enable = true;
workspaces = {
"1" = { name = "Terminal"; };
"2" = { name = "Browser"; };
"3" = { name = "Code"; };
};
};
}
AI Tooling
{ ... }: {
decknix.cli.auggie.enable = false;
}
Git Features
{ ... }: {
# Disable Forge (GitHub PRs in Emacs)
programs.emacs.decknix.magit.forge.enable = false;
# Disable code-review
programs.emacs.decknix.magit.codeReview.enable = false;
}
System-Level Features
# ~/.config/decknix/local/system.nix
{ ... }: {
# Disable Emacs daemon
services.emacs.decknix.enable = false;
# Disable AeroSpace system optimisations
decknix.services.aerospace.enable = false;
}
Using lib.mkForce
When a simple override doesn't work (because another module sets a value explicitly), use mkForce:
{ lib, ... }: {
programs.emacs.decknix.enable = lib.mkForce false;
}
Use sparingly — in most cases, a normal override is sufficient because the framework uses lib.mkDefault.
Modules Overview
Decknix is organised into modular components, each responsible for a specific area of your environment. Every module uses lib.mkDefault so you can override any setting.
Module Categories
Home-Manager Modules
| Module | Description | Page |
|---|---|---|
| Emacs | Full IDE with 13+ sub-modules, profiles, daemon | Emacs → |
| Vim | Whitespace cleanup, skim fuzzy finder | Vim → |
| Shell & Terminal | Zsh, Starship prompt, completions | Shell → |
| Git | Delta diffs, global config, LFS | Git → |
| Window Management | AeroSpace, Hammerspoon, Spaces | WM → |
| AI Tooling | Augment Code agent, MCP servers, Agent Shell | AI → |
Darwin (System) Modules
| Module | Description |
|---|---|
| System Defaults | Packages (vim, git, curl, skim), Nerd Fonts, Dock/Finder prefs |
| AeroSpace System | Disables Stage Manager, Mission Control shortcuts, separate Spaces |
| Emacs Daemon | Background Emacs service via launchd, ec wrapper command |
| CLI Module | Installs decknix binary, generates extensions config |
Core Options
| Option | Description | Default |
|---|---|---|
decknix.role | Bootstrap template: "developer", "designer", "minimal" | "developer" |
decknix.username | Your macOS username (set automatically by mkSystem) | — |
decknix.hostname | Machine hostname | — |
Editor Profiles
Instead of toggling individual modules, choose a profile tier:
Emacs Profiles
| Profile | Modules Included |
|---|---|
minimal | core, completion, editing, UI, undo, project |
standard | minimal + development, magit, treemacs, languages, welcome |
full (default) | standard + LSP, org-mode, HTTP client, agent-shell |
custom | Disables framework Emacs — bring your own config |
Vim Profiles
| Profile | Modules Included |
|---|---|
minimal | Base config (exrc, line numbers, secure) |
standard (default) | minimal + whitespace + skim |
custom | Disables framework Vim — bring your own config |
# Change profiles
{ ... }: {
decknix.editors.emacs.profile = "standard";
decknix.editors.vim.profile = "minimal";
}
Default Packages
Installed for all users regardless of role:
coreutils · curl · wget · tree · jq · ripgrep · gh
System-level: vim · git · curl · skim
Fonts: JetBrains Mono Nerd Font
Editors
Decknix provides opinionated configurations for two editors: Emacs (full IDE experience) and Vim (lightweight enhancements).
Emacs
A batteries-included Emacs configuration with 13+ modules covering completion, git, LSP, languages, org-mode, and more. Runs as a background daemon on macOS.
Highlights:
- Modern completion stack (Vertico, Consult, Corfu)
- Git integration via Magit + Forge (GitHub PRs)
- LSP support for Kotlin, Java, and more via Eglot
- 30+ language modes with syntax highlighting
- Org-mode presentations
- REST API client
- AI Agent Shell — multi-session AI interface
Vim
Lightweight enhancements on top of the base Vim config.
Highlights:
- Trailing whitespace cleanup (vim-better-whitespace)
- Fuzzy file finder (skim)
- Base config: line numbers, exrc, secure mode
Profiles
Both editors support tiered profiles to control how much framework config is applied:
{ ... }: {
decknix.editors.emacs.profile = "standard"; # minimal | standard | full | custom
decknix.editors.vim.profile = "minimal"; # minimal | standard | custom
}
Setting custom disables the framework's editor config entirely, letting you bring your own.
Emacs
Decknix provides a modern, batteries-included Emacs experience with 13+ modules, background daemon, and three profile tiers.
Modules
| Module | Description | Profile |
|---|---|---|
| Core | Modus theme, line numbers, better defaults | minimal+ |
| Completion | Vertico, Consult, Corfu, Embark | minimal+ |
| Editing | Smartparens, Crux, Move-text, EditorConfig | minimal+ |
| UI | Which-key, Helpful, Nerd-icons | minimal+ |
| Undo | undo-fu, vundo (visual undo tree) | minimal+ |
| Project | Project management and navigation | minimal+ |
| Welcome | Startup screen with keybinding cheat sheet | standard+ |
| Development | Flycheck, Yasnippet | standard+ |
| Magit | Git interface, Forge (GitHub PRs), code-review | standard+ |
| Treemacs | Project file tree with git integration | standard+ |
| Languages | 30+ language modes with syntax highlighting | standard+ |
| LSP | Eglot, kotlin-ls, jdt-ls, dape (debugging) | full |
| Org-mode | Modern styling, presentations (Olivetti) | full |
| HTTP | REST client, jq integration, org-babel | full |
| Agent Shell | AI agent interface (Augment Code) | full |
Key Bindings — Quick Reference
Navigation & Search
| Key | Action |
|---|---|
C-s | Search in buffer (consult-line) |
C-x b | Switch buffer with preview |
M-s r | Project-wide ripgrep search |
M-y | Browse kill ring |
C-. | Context actions (Embark) |
Git (Magit)
| Key | Action |
|---|---|
C-x g | Magit status |
@ f f | Fetch forge topics (PRs/issues) |
@ c p | Create pull request |
@ l p | List pull requests |
File Tree (Treemacs)
| Key | Action |
|---|---|
C-x t t | Toggle treemacs |
C-x t f | Find current file in tree |
LSP / Code
| Key | Action |
|---|---|
C-c l r | Rename symbol |
C-c l a | Code actions |
C-c l f | Format region |
C-c l F | Format buffer |
C-c l d | Show documentation |
Debugging (dape)
| Key | Action |
|---|---|
C-c d d | Start debugger |
C-c d b | Toggle breakpoint |
C-c d n | Step over |
C-c d s | Step in |
C-c d c | Continue |
Editing
| Key | Action |
|---|---|
C-a | Smart home (Crux) |
C-c d | Duplicate line |
M-up/down | Move line/region |
C-/ | Undo |
C-? | Redo |
C-x u | Visual undo tree (vundo) |
Org-mode
| Key | Action |
|---|---|
F5 or C-c p | Start/stop presentation |
n / p | Next/previous slide |
Languages
30+ languages with syntax highlighting:
| Category | Languages |
|---|---|
| Primary | Kotlin, Java, Scala, SQL, Terraform/HCL, Shell, Nix, Python |
| Data | JSON, YAML, TOML, XML, Markdown |
| Web | HTML, CSS/SCSS/LESS, JavaScript, TypeScript, JSX, Vue, Svelte |
Emacs Daemon
Configured in modules/darwin/emacs.nix, enabled by default on macOS. Runs as a background launchd service — no Dock icon, no Cmd+Tab entry.
ec filename # Open file in Emacs
ec -c -n # New GUI frame
ec -c -n file.txt # Open file in new GUI frame
ec -t file.txt # Open in terminal
emacsclient -c # Create new GUI frame
GUI frames appear in the Dock while open; closing a frame doesn't kill the daemon.
| Option | Default | Description |
|---|---|---|
services.emacs.decknix.enable | true | Enable Emacs daemon |
services.emacs.decknix.package | pkgs.emacs | Emacs package to use |
services.emacs.decknix.additionalPath | [] | Extra PATH entries for daemon |
Module Options Reference
Disabling Modules
{ ... }: {
programs.emacs.decknix.enable = false; # ALL emacs config
programs.emacs.decknix.welcome.enable = false; # Welcome screen
programs.emacs.decknix.magit.enable = false; # Git interface
programs.emacs.decknix.magit.forge.enable = false; # Just Forge
programs.emacs.decknix.completion.enable = false; # Completion stack
programs.emacs.decknix.treemacs.enable = false; # File tree
programs.emacs.decknix.undo.enable = false; # Undo enhancements
programs.emacs.decknix.editing.enable = false; # Editing enhancements
programs.emacs.decknix.development.enable = false; # Flycheck/Yasnippet
programs.emacs.decknix.ui.enable = false; # UI enhancements
programs.emacs.decknix.ui.icons.enable = false; # Just icons
programs.emacs.decknix.org.enable = false; # Org enhancements
programs.emacs.decknix.lsp.enable = false; # LSP/IDE
programs.emacs.decknix.http.enable = false; # REST client
programs.emacs.decknix.languages.enable = false; # All languages
}
Customisation
{ pkgs, ... }: {
# Add your own packages
programs.emacs.extraPackages = epkgs: [ epkgs.evil epkgs.lsp-mode ];
# Add your own config
programs.emacs.extraConfig = ''
(evil-mode 1)
(setq my-custom-variable t)
'';
}
Note: Evil mode (Vim emulation) is not included by default. Add it in your personal config as shown above.
Vim
Decknix provides lightweight Vim enhancements on top of a sensible base config.
Base Config (All Profiles)
set exrc— load project-local.vimrcset secure— restrict commands in project.vimrc- Line numbers enabled
Whitespace Module
Plugin: vim-better-whitespace
Automatically strips trailing whitespace on save.
| Option | Default | Description |
|---|---|---|
programs.vim.decknix.whitespace.enable | true (standard profile) | Enable whitespace cleanup |
programs.vim.decknix.whitespace.stripModifiedOnly | true | Only strip modified lines |
programs.vim.decknix.whitespace.confirm | false | Prompt before stripping |
Skim Module
Plugin: skim (fuzzy finder)
Integrates skim into Vim for fast file and buffer searching.
| Option | Default | Description |
|---|---|---|
programs.vim.decknix.skim.enable | true (standard profile) | Enable skim integration |
Profiles
| Profile | Includes |
|---|---|
minimal | Base config only |
standard (default) | Base + whitespace + skim |
custom | Disables framework Vim entirely |
{ ... }: {
decknix.editors.vim.profile = "minimal";
}
Adding Your Own Config
{ ... }: {
programs.vim = {
enable = true;
plugins = [ pkgs.vimPlugins.vim-surround ];
extraConfig = ''
set relativenumber
'';
};
}
Shell & Terminal
Decknix configures Zsh as the default shell with modern enhancements.
Zsh
Enabled by default with:
- Completion — case-insensitive, menu-driven completion
- Autosuggestion — fish-like suggestions from history
- Syntax highlighting — command validation as you type
- History — 50,000 entries, shared across sessions, prefix-based search
Starship Prompt
Starship provides a fast, customisable prompt showing git status, language versions, and more.
Customise the prompt character:
{ ... }: {
programs.starship.settings.character = {
success_symbol = "[➜](bold green)";
error_symbol = "[✗](bold red)";
};
}
Inline Timestamp
The prompt line shows a wall-clock timestamp grouped with the command-duration so they read as a pair:
~/.config/decknix on ☁️ lachlan@example.com · 2026-04-30T14:32:15 · took 45s
➜
Configure via programs.starship.decknix.timestamp.*:
{ ... }: {
programs.starship.decknix.timestamp = {
enable = true; # default
format = "%Y-%m-%dT%H:%M:%S"; # default (ISO 8601)
separator = " · "; # default
style = "dimmed"; # default
};
}
format accepts any Chrono strftime string. Common alternatives:
| Format | Renders as |
|---|---|
%Y-%m-%dT%H:%M:%S | 2026-04-30T14:32:15 |
%T | 14:32:15 |
%H:%M | 14:32 |
%a %H:%M | Tue 14:32 |
%I:%M %p | 02:32 PM |
Set enable = false to drop the timestamp and revert cmd_duration
to upstream defaults.
Delta (Diff Viewer)
Delta is configured as the default git pager, providing syntax-highlighted diffs.
direnv
direnv is enabled by default, so cd-ing into a
directory with an authorised .envrc automatically loads its environment
(and unloads it on the way out). The zsh hook is wired automatically — no
manual eval "$(direnv hook zsh)" needed.
nix-direnv is included for
fast, cached use nix / use flake support, keeping the dev-shell alive
as a GC root so it reloads instantly instead of re-evaluating on every
cd. A minimal flake-based .envrc:
# .envrc — run `direnv allow` once to authorise it
use flake
Disable it (or drop nix-direnv) via your personal config:
{ ... }: {
programs.direnv.enable = false; # disable entirely
# or keep direnv but skip nix-direnv:
# programs.direnv.nix-direnv.enable = false;
}
Default Shell Aliases
Decknix doesn't impose shell aliases — add your own:
{ ... }: {
programs.zsh.shellAliases = {
ll = "ls -la";
gs = "git status";
gp = "git pull --rebase";
};
}
Extra Init
Add custom shell initialization:
{ ... }: {
programs.zsh.initExtra = ''
# Source work credentials
[[ -f ~/.config/secrets/env.sh ]] && source ~/.config/secrets/env.sh
'';
}
Session Variables
{ ... }: {
home.sessionVariables = {
EDITOR = "emacsclient -c";
VISUAL = "emacsclient -c";
};
}
Git
Decknix provides a sensible Git configuration with modern tooling.
Default Settings
| Setting | Value | Description |
|---|---|---|
init.defaultBranch | main | Default branch name |
pull.rebase | true | Rebase on pull instead of merge |
push.autoSetupRemote | true | Auto-create remote tracking branch |
core.pager | delta | Syntax-highlighted diffs |
lfs.enable | true | Git Large File Storage |
Delta Integration
Delta provides beautiful, syntax-highlighted diffs in the terminal with line numbers.
Customising Git
# ~/.config/decknix/local/home.nix
{ ... }: {
programs.git.settings = {
user.email = "you@example.com";
user.name = "Your Name";
core.editor = "emacsclient -c";
};
}
Conditional Includes
Use different identities for different directories:
{ ... }: {
programs.git.includes = [{
condition = "gitdir:~/Code/work/";
contents = {
user.email = "you@company.com";
user.name = "Your Name";
commit.gpgsign = true;
};
}];
}
Magit (Emacs Git Interface)
The Emacs module includes Magit — a full Git interface inside Emacs:
C-x g→ Git status- Stage, commit, push, pull, rebase — all from keyboard
- Forge — manage GitHub PRs and issues without leaving Emacs
- code-review — inline PR review with comments
See Secrets & Authentication for Forge token setup.
GitHub CLI
The gh CLI is installed by default. Decknix also auto-configures authenticated GitHub API access for Nix itself via decknix.nix.githubAuth (see Secrets).
Window Management
Decknix supports several window management approaches for macOS, from full tiling to simple snapping. Pick the style that suits your workflow — they can be used individually or combined.
| Tool | Style | Source | Decknix module |
|---|---|---|---|
| Amethyst | Automatic tiling (xmonad-style) | nix-casks | — (package only) |
| AeroSpace | Manual tiling (i3-style) | nixpkgs | decknix.wm.aerospace |
| Rectangle | Window snapping (keyboard shortcuts) | nix-casks | — (package only) |
| SpaceId | Menu-bar space indicator | nix-casks | — (package only) |
| Native macOS | Stage Manager, Split View, Spaces | built-in | — |
Amethyst
Amethyst is an automatic tiling window manager for macOS in the style of xmonad. Windows are arranged automatically into layouts (tall, wide, fullscreen, column, BSP, etc.) and reflow when windows are added or removed.
Amethyst is the recommended starting point — it works with native macOS
Spaces, requires no SIP disabling, and is fully configurable via its
preferences pane or defaults write.
Installation
Amethyst is not in nixpkgs; install it via nix-casks:
# home.nix (personal overrides)
{ pkgs, inputs, ... }: {
home.packages =
with inputs.nix-casks.packages.${pkgs.stdenv.hostPlatform.system}; [
amethyst
];
}
Configuration
Amethyst stores its configuration in macOS user defaults
(com.amethyst.Amethyst). Key settings:
| Default key | Description |
|---|---|
layouts | Active layout cycle (e.g., tall, wide, fullscreen, column) |
mod1 | Primary modifier (default: option + shift) |
mod2 | Secondary modifier (default: ctrl + option + shift) |
enables-layout-hud | Show layout name on switch |
window-margins | Enable gaps between windows |
floating | List of app bundle IDs to float |
Note — Glide: Glide is a newer tiling WM by the same author as Amethyst. It is not yet recommended for regular use and is not included in the default configuration. If you want to experiment with it, install via nix-casks and use it in place of Amethyst.
AeroSpace
AeroSpace is a manual tiling window manager inspired by i3. Unlike Amethyst's automatic layouts, AeroSpace gives you direct control over window placement using keyboard commands. It manages its own virtual workspaces (separate from macOS Spaces).
Decknix provides both home-manager and darwin modules for AeroSpace.
Enabling
# home.nix
{ ... }: {
decknix.wm.aerospace.enable = true;
}
Options
| Option | Default | Description |
|---|---|---|
decknix.wm.aerospace.enable | false | Enable AeroSpace |
decknix.wm.aerospace.prefixKey | "cmd+alt" | Prefix key for commands |
decknix.wm.aerospace.keyStyle | "emacs" | "emacs" (arrows) or "vim" (hjkl) |
decknix.wm.aerospace.showModeHints | false | Show mode notifications |
decknix.wm.aerospace.fuzzyPicker.enable | true | Spotlight-like workspace/window picker |
Workspaces
Define named workspaces with optional monitor assignment:
{ ... }: {
decknix.wm.aerospace.workspaces = {
"1" = { name = "Terminal"; };
"2" = { name = "Browser"; };
"3" = { name = "Code"; };
"4" = { name = "Chat"; monitor = "secondary"; };
};
}
Default workspaces: 1–5 (main, web, term, mail, chat) + D, E, N, M, S (decknix, emacs, notes, music, system).
System-Level Settings
The darwin module optimises macOS for tiling:
| Option | Default | Effect |
|---|---|---|
decknix.services.aerospace.disableStageManager | true | Prevents conflicts |
decknix.services.aerospace.disableSeparateSpaces | true | Multi-monitor support |
decknix.services.aerospace.disableMissionControlShortcuts | true | Frees Ctrl+arrow keys |
decknix.services.aerospace.autohideDock | true | Maximises screen space |
Rectangle
Rectangle is a lightweight window snapping tool. It provides keyboard shortcuts for half-screen, quarter-screen, thirds, and other arrangements — similar to Windows snap or Spectacle (which Rectangle succeeds).
Rectangle is a good choice if you prefer manual control without full tiling. It pairs well with Amethyst (use Amethyst for your main workspace and Rectangle shortcuts for quick one-off snaps).
Installation
# home.nix
{ pkgs, inputs, ... }: {
home.packages =
with inputs.nix-casks.packages.${pkgs.stdenv.hostPlatform.system}; [
rectangle
];
}
Rectangle is configured via its preferences pane. No Decknix module is needed.
SpaceId
SpaceId shows the current macOS Space number in the menu bar. Useful alongside any window manager to keep track of which Space you're on.
Installation
# home.nix
{ pkgs, inputs, ... }: {
home.packages =
with inputs.nix-casks.packages.${pkgs.stdenv.hostPlatform.system}; [
spaceid
];
}
Hammerspoon
Hammerspoon provides Lua-based macOS automation. Decknix uses it for space navigation bindings.
{ ... }: {
decknix.wm.hammerspoon = {
enable = true;
modifier = "Meta + Ctrl";
};
}
Generates Lua configuration with space navigation bindings.
Spaces
WM-agnostic multi-monitor workspace management with named space groups and picker scripts:
{ ... }: {
decknix.wm.spaces.workspaces = {
dev = {
name = "Development";
startSpace = 1;
spaces = [ "terminal" "editor" "browser" ];
key = "d";
};
};
}
Generates space picker scripts with shortcodes for quick switching. Works with any WM or standalone.
AI Tooling
Decknix provides a declarative, Nix-managed AI development environment — from CLI agent configuration to a full Emacs-native agent interface with session management, work context awareness, and prompt engineering tools.
What's Included
| Component | Description | Page |
|---|---|---|
| Auggie CLI | Augment Code agent with Nix-managed settings and MCP servers | Configuration → |
| Agent Shell | Emacs-native multi-session AI interface (7 sub-modules) | Agent Shell → |
Design Principles
- Declarative first — all configuration lives in Nix.
decknix switchreproduces your entire AI setup on any machine. - Runtime-mutable — settings are copied (not symlinked) so tools can modify them at runtime. The next
decknix switchresets to the Nix-managed baseline. - Composable — each component is independently toggleable. Use the CLI without Emacs, or Emacs without MCP servers.
- Session-as-first-class — AI conversations are persistent objects with tags, context items, and metadata — not disposable chat buffers.
Architecture
┌─────────────────────────────────────────────────┐
│ Nix Configuration (agent-shell.nix, auggie.nix)│
│ ┌──────────┐ ┌──────────┐ ┌────────────────┐│
│ │ Settings │ │ MCP │ │ Commands & ││
│ │ & Model │ │ Servers │ │ Templates ││
│ └─────┬─────┘ └─────┬────┘ └───────┬────────┘│
└────────┼──────────────┼───────────────┼──────────┘
▼ ▼ ▼
~/.augment/ ~/.augment/ ~/.claude/commands/
settings.json settings.json ~/.emacs.d/snippets/
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ Emacs Agent Shell │
│ ┌──────┐ ┌─────────┐ ┌────────┐ ┌───────────┐│
│ │Core │ │Sessions │ │Prompts │ │Context ││
│ │ACP │ │Tags │ │Commands│ │CI/PR/Issue││
│ └──────┘ └─────────┘ └────────┘ └───────────┘│
└─────────────────────────────────────────────────┘
Quick Start
AI tooling is enabled by default in the full Emacs profile. To start:
C-c A a → Start an agent session
C-c A s → Session picker (live + saved + new)
C-c A ? → Full keybinding reference
For CLI-only usage without Emacs:
auggie # Interactive agent session
auggie session list # List saved sessions
auggie session resume ID # Resume a session
Next Steps
- Configuration — Auggie CLI settings, MCP servers, model selection
- Agent Shell Overview — The Emacs agent interface
- Vision — Where this is heading
AI Configuration
All AI tooling is configured declaratively in Nix and deployed via decknix switch.
Auggie CLI
Enabling
{ ... }: {
decknix.cli.auggie.enable = true;
}
Settings
{ ... }: {
decknix.cli.auggie.settings = {
model = "opus4.6";
indexingAllowDirs = [
"~/tools/decknix"
"~/Code"
];
};
}
Settings are written to ~/.augment/settings.json. The file is copied (not symlinked) so auggie can modify it at runtime; the next decknix switch overwrites with the Nix-managed version.
MCP Servers
Declaratively configure Model Context Protocol servers:
{ ... }: {
decknix.cli.auggie.mcpServers = {
context7 = {
type = "stdio";
command = "npx";
args = [ "-y" "@upstash/context7-mcp@latest" ];
env = {};
};
"gcp-monitoring" = {
type = "stdio";
command = "npx";
args = [ "-y" "gcp-monitoring-mcp" ];
env.GOOGLE_APPLICATION_CREDENTIALS = "~/.config/gcloud/credentials.json";
};
};
}
MCP servers are written into the mcpServers section of ~/.augment/settings.json.
Slack MCP Workspaces
Connect auggie to one or more Slack workspaces using the official Slack MCP server:
{ ... }: {
decknix.cli.auggie.slack.workspaces = {
acme-corp = {
clientId = "3660753192626.123456";
description = "ACME Corp team workspace";
};
personal = {
clientId = "3660753192626.789012";
};
};
}
Each workspace generates a slack-<name> entry in mcpServers pointing at https://mcp.slack.com/mcp with the workspace's CLIENT_ID for OAuth authentication.
Setup requirements:
- Create or reuse a Slack app at api.slack.com/apps
- Enable OAuth with appropriate scopes (e.g.,
search:read.public,chat:write,channels:history) - Publish as an internal app or to the Slack Marketplace
- Copy the Client ID from the app's OAuth settings
Multiple workspaces merge naturally — define some in your org config, others in your personal config, and they all appear in settings.json.
Viewing Configured Servers
From Emacs: C-c A S opens a formatted buffer showing all configured MCP servers with their type, command, args, and environment variables.
Runtime vs Nix-Managed
| Source | Persists across decknix switch? | How to add |
|---|---|---|
| Nix config | ✅ Yes | decknix.cli.auggie.mcpServers |
auggie mcp add | ❌ No (temporary) | Runtime command |
Agent Shell Module
The Emacs agent-shell module is enabled by default in the full profile:
{ ... }: {
programs.emacs.decknix.agentShell = {
enable = true; # Core agent-shell.el + ACP
manager.enable = true; # Tabulated session dashboard
workspace.enable = true; # Dedicated tab-bar workspace
attention.enable = true; # Mode-line attention tracker
templates.enable = true; # Yasnippet prompt templates
commands.enable = true; # Nix-managed slash commands
context.enable = true; # Work context panel (issues, PRs, CI)
};
}
Each sub-module can be independently disabled. See Agent Shell Overview for details on each component.
Per-Purpose Provider & Model
Automated agent launches (PR reviews, bot-authored PR reviews) and the
interactive new-session path can pin a specific (provider, model, mode) triple via Nix, independent of the interactive
decknix-agent-default-provider. Three purposes ship today:
| Purpose | Trigger | Default provider | Default model | Default mode |
|---|---|---|---|---|
pr-review | C-c A c r, sidebar Requests row, batch processor | claude-code | sonnet | auto |
bot-pr-review | Auto-review dispatch on bot-authored PRs, or matched by author heuristic | claude-code | sonnet | auto |
new-session | Interactive / QUICK C-c A n (its provider also feeds decknix-agent-default-provider) | claude-code | null | auto |
{ ... }: {
programs.emacs.decknix.agentShell.purposes = {
# Human PR reviews go through Claude with opus for depth.
pr-review = { provider = "claude-code"; model = "opus"; mode = "auto"; };
# Bot diffs are shallow — pin the cheapest capable model.
bot-pr-review = { provider = "claude-code"; model = "haiku"; mode = "auto"; };
# Start new interactive sessions on Claude in auto (no per-command prompts).
new-session = { provider = "claude-code"; mode = "auto"; };
};
}
Validation. All three fields are validated at daemon start:
providermust be a registered provider id (built-ins:auggie,claude-code,pi). Unknown values coerce todecknix-agent-default-providerwith a warning to*Warnings*.modelmust appear indecknix-agent-known-modelsfor the chosen provider (or benullto defer to the provider default). Unknown values drop tonilwith a warning.modeis a session/permission mode honoured only by providers that expose one — todayclaude-code, whose ids aredefault,auto,acceptEdits,bypassPermissions, andplan. For providers without session modes (Auggie, Pi) it drops tonilat boot with a warning. Set tonullto keep the provider's own default.
Resume semantics. For launch-flag providers (Auggie) the model is
appended as --model <id>; for flagless providers (Claude, Pi) it is
replayed over ACP once the session reports ready. The permission
mode is baked into the session config and applied by agent-shell once
the session reports ready. Either way, once you switch mid-session
with C-c C-v (model) or C-c C-m (mode), that per-conversation
choice persists in ~/.config/decknix/agent-sessions.json and wins
over the purpose default on both resume and fork.
Scope. The two *-review purposes are consulted by the automated
review launchers; new-session seeds interactive C-c A n. C-c A f
(fork) inherits the source conversation's persisted model/mode (falling
back to the new-session defaults), and the sidebar worktree w s
action keeps decknix-agent-default-provider with no model pin.
See Model Selection
for the full override-lever hierarchy.
Custom Commands
Nix-managed commands are deployed to ~/.claude/commands/ (the shared slash-command location read natively by both Claude Code and Auggie) and also to ~/.pi/agent/prompts/ (where Pi reads them as /name prompt templates), so a single source covers every supported agent. User-created commands (regular files) coexist in each directory and are not affected by decknix switch.
# Commands are defined in agent-shell.nix and deployed automatically.
# To add your own at runtime:
# C-c c n → Create new command (opens template in ~/.claude/commands/)
See Productivity for the full command framework.
Claude Permissions
Claude Code prompts before running any Bash command unless it matches a rule in
~/.claude/settings.json (permissions.allow). Skills ship executable helper
scripts (deployed 755 via decknix.cli.agentSync with executable = true),
so without an allow rule Claude asks for permission every session before running
them.
By default decknix auto-allowlists every Nix-installed executable tool it
manages — each executable agent-sync file becomes a narrow
Bash(<abs-path>:*) prefix rule (framework- and org-registered scripts alike):
{ ... }: {
decknix.ai.claude = {
enable = true;
# Auto-allow decknix/Nix-installed executable skill scripts (default: true).
permissions.allowManagedTools = true;
# Extra rules merged in alongside the managed-tool rules.
permissions.allow = [
"Bash(gh pr view:*)"
];
# DENY rules always win over allow — the backstop for a broad allow-list.
permissions.deny = [
"Bash(sudo:*)"
"Read(~/.config/decknix/**/secrets/**)"
];
};
}
Both lists are deep-merged into ~/.claude/settings.json — decknix updates
only .permissions.allow and .permissions.deny (each union + de-duplicated)
and leaves every other key untouched, because Claude mutates this file at
runtime (e.g. it writes skipDangerousModePermissionPrompt). Set
allowManagedTools = false to manage the allowlist entirely by hand.
Allowlist vs. blacklist model
A path-scoped allowlist rots the moment a managed tool moves on disk: every
stale Bash(<old-abs-path>:*) rule silently matches nothing, and Claude
re-prompts for the relocated tool every session. Two ways to avoid that:
-
Allowlist (default). Keep
allowManagedTools = trueand let the auto-derived rules track each tool's current absolute path. They regenerate on everydecknix switch, so a move updates the rule automatically — provided the tool stays a managed executable agent-sync file. -
Blacklist. Allow the common tools broadly and lean on
permissions.denyas the backstop:decknix.ai.claude.permissions = { allowManagedTools = false; # broad Bash below supersedes them allow = [ "Bash" "Read" "Edit" "Write" ]; deny = [ "Read(~/.config/decknix/**/secrets/**)" "Bash(sudo:*)" "Bash(rm -rf /:*)" "Bash(rm -rf ~:*)" ]; };A bare
Bashallow can't rot when a script moves, so this trades a maintained allowlist for a small never-do denylist. Two caveats:denyis evaluated beforeallowand always wins, but the host's own destructive-command classifier still runs independently — it remains the real backstop even under a broad allow.- A
Read(...)deny guards the Read/Edit tool path only. Under a broadBashallow a shellcat/<can still reach a denied file, so treat secret denies as defence-in-depth (and against accidental whole-dir slurps), not a hard boundary. Keep secrets0600and out of the Nix store regardless.
Leave defaultMode at default for the blacklist model. bypassPermissions
would skip permission evaluation entirely — including your own deny rules —
which is the opposite of what a denylist is for.
Claude MCP Servers
Configure MCP servers for Claude Code globally — every workspace inherits them
without a per-repo .mcp.json:
{ ... }: {
decknix.ai.claude = {
enable = true;
mcpServers = {
# Atlassian (Jira + Confluence) via the mcp-remote bridge. First
# invocation opens a browser flow for OAuth; after that Claude has
# native Jira/Confluence tools and no longer shells out to
# `auggie --print --ask` for issue lookups (the previous workaround
# was ~2 min per call).
atlassian = {
type = "stdio";
command = "npx";
args = [ "-y" "mcp-remote" "https://mcp.atlassian.com/v1/sse" ];
};
};
};
}
The shape mirrors decknix.cli.auggie.mcpServers — stdio
servers use type / command / args / env; remote servers use
type = "http" (or "sse") plus url and optional headers.
Entries are deep-merged into ~/.claude.json (.mcpServers) on every
decknix switch. Claude mutates this file heavily at runtime (skillUsage,
cached* caches, OAuth tokens, migration flags), so decknix only touches
.mcpServers and leaves the ~40 other runtime-managed keys alone:
- Nix-declared entries win against runtime-added entries with the same name.
- Runtime-added entries with unrelated names are preserved.
- Removing an entry from Nix does not remove it from
~/.claude.json(Claude may have converged on it independently); purge those with/mcp remove <name>inside Claude.
Reach for a workspace-local .mcp.json only when a server should be strictly
project-local (e.g. a repo-scoped test harness). Global config here keeps
personal + org tooling consistent across every project you open.
Agent Shell
The Emacs Agent Shell is a native, multi-session AI agent interface built on agent-shell.el and the Augment Code Protocol (ACP). It turns Emacs into a first-class AI development environment where sessions are persistent, context-aware, and deeply integrated with your workflow.
Why Not Just a Chat Buffer?
Most AI integrations treat conversations as disposable text. Agent Shell treats them as first-class objects:
- Sessions persist — resume any conversation from days ago with full context
- Sessions have metadata — tags, pinned issues, CI status, review threads
- Sessions are searchable — find any past session by keyword or tag
- Sessions are composable — structured prompts via templates and slash commands
- Sessions are work-aware — auto-detect issues, PRs, and Jira tickets from conversation text
Package Ecosystem
Agent Shell is assembled from 6 packages using tiered sourcing:
| Package | Source | Purpose |
|---|---|---|
shell-maker | nixpkgs unstable | Comint-like shell buffer management |
acp | nixpkgs unstable | Augment Code Protocol client |
agent-shell | nixpkgs unstable | Core agent interface |
agent-shell-manager | Custom derivation | Tabulated session dashboard |
agent-shell-workspace | Custom derivation | Dedicated tab-bar workspace |
agent-shell-attention | Custom derivation | Mode-line attention tracker |
Plus ~1,800 lines of custom Elisp in agent-shell.nix providing sessions, tags, compose, commands, templates, and context awareness.
Layers
The implementation is organised into 5 layers, each building on the previous:
| Layer | Name | What It Provides | Page |
|---|---|---|---|
| 1 | Foundation | Core shell, ACP protocol, package sourcing | Foundation → |
| 2 | Multi-Session | Session picker, resume, history, quit | Multi-Session → |
| 3 | Productivity | Compose buffer, templates, commands, tags | Productivity → |
| 4 | Integration | MCP servers, declarative tool config | Integration → |
| 5 | Context | Issues, PRs, CI status, review threads | Context → |
Quick Reference
C-c A a Start / switch to agent
C-c A s Session picker (live + saved + new)
C-c A e Compose multi-line prompt
C-c A ? Full keybinding help
C-c A I Context panel (issues, PRs, CI)
Inside an agent-shell buffer, drop the A prefix: C-c s, C-c e, C-c ?, etc.
Guided Tour
A "screencast" of the Agent Shell, grouped by topic — from the welcome screen to opening the sidebar and running the everyday flows. Each step shows the keys pressed and a mock of what appears. Colours are the real Emacs face colours (same palette as the Sidebar Layouts); the frames are mock-ups, so spacing is approximate — colour and layout are the contract.
Legend — provider glyph A Auggie · C Claude · P Pi. Session state:
{dm}○{/} initializing {y}◐{/} working {r}◐{/} waiting (needs input)
{g}●{/} ready {ac}●{/} finished {r}●{/} killed
Getting oriented
The welcome screen
Emacs opens on the *decknix* buffer. Press C-c w to return to it any time.
emacs
{hd} __ _ __ __ _ ___ ___{/}
{ac} __| |___ / | | ' \| \/ || | / __/ __| {/}
{c} / _` / -_) || | ' <| |\/| || || \__ \__ \ {/}
{br} \__,_\___|_||_|_|\_\_| |_||_|/_|___/___/ {/}
{bd}Welcome to dEckMACS{/} — a modern, batteries-included Emacs config
{dm}Vertico • Consult • Marginalia • Corfu • Embark • Magit{/}
{c bd}1{/} Navigation & Search {dm}C-s M-s r C-x b{/}
{c bd}2{/} Editing {dm}C-/ C-? C-x u{/}
{c bd}3{/} Completion & Code {dm}TAB C-. M-n/p{/}
{c bd}4{/} Git (Magit) {dm}C-x g C-x M-g s / u{/}
{c bd}5{/} Buffers & Windows {dm}C-x C-f C-x k C-x 1{/}
{c bd}6{/} Help & Discovery {dm}C-h f C-h k C-h t{/}
{dm}Press 1-6 for full cheat sheets • r refresh • q quit{/}
{dm}C-h ? help • C-x b buffers • C-x C-f files • C-c w this screen{/}
The shipping welcome buffer. Numbers 1–6 open per-topic cheat sheets.
…with agents surfaced (proposed)
The welcome screen is a natural home for a live agent summary and quick-actions
into the Agent Shell. A proposed addition — a category 7 plus a status strip that
mirrors the sidebar's Live section:
{c bd}6{/} Help & Discovery {dm}C-h f C-h k C-h t{/}
{c bd}7{/} {ac bd}AI Agents{/} {dm}C-c A a start C-c A w sidebar C-c A s sessions{/}
{dm}Agents{/} {y}◐ 2 working{/} {r}◐ 1 waiting{/} {g}● 3 ready{/} {dm}· C-c A w to open{/}
Proposed, not yet shipped — mocked to show the intent (see How It Compares → roadmap).
Open the Agents sidebar
C-c A w
The four-section workspace sidebar opens on the right: Requests → WIP → Live → Sessions.
{hd}Requests (3){/} ⇅
{c}⎇ {/} {y}2d{/} upside{gd}#16570{/} {g}●{/}{gd bd}@{/}{ac}◉{/} {gd}CSE-201: refactor token cache{/}
{dm bd}↓ {/}{r}15d{/} upside#16568 {bo}π{/} {dm}bump: dependency update{/}
{dm bd}↓ {/} {y}1d{/} reapit#123 {r}◐{/}{c bd}@{/} CSE-204: fix pubsub timeout
{hd}WIP (2){/}
{g bd}⎇*{/} {g}●{/} decknix{gd}#812{/} [3⬆ 2✓]
{c}⎇ {/} {y}★{/} decknix-config#77 {y}draft{/}
{hd}Live (2){/}
> {y}A{/} {y}◐{/} feature/token-cache {rp}decknix{/} [2⬆ 1✓] {r bd}📥1{/}
{g}C{/} {g}●{/} review/auth {rp}decknix{/} {sg bd}↩{/}
{hd}Sessions (18){/} {dm}…recent by tag / date{/}
Colour is load-bearing: green approved/ready, red needs-you/CI-fail, yellow draft/working, blue branch, pink bot. ⎇* = branch live in a session; ◉ = a review session is already open on that PR.
Running a session
Start a session
C-c A n
A provider picker appears (Vertico). Choose the agent to launch.
{dm}Start agent (provider):{/}
{ac}>{/} {g}C{/} Claude Code {dm}(default · claude-code){/}
{y}A{/} Auggie {dm}(Augment Code){/}
{c}P{/} Pi {dm}(Contextual AI){/}
{dm}?{/} Gemini {dm}(Google){/}
Pick C and a fresh Claude session buffer opens; the Live section gains a ○→◐ row.
Watch its sub-agents
C-c A w
As the agent spins up sub-agents, the Live section shows each with its own state — colour tells you at a glance who needs you and who's done.
{hd}Live (1 · 3 sub){/}
> {g}C{/} {y}◐{/} feature/token-cache {rp}decknix{/}
{dm}├─{/} {g}●{/} explore {dm}mapped cache call-sites{/}
{dm}├─{/} {y}◐{/} implement {dm}editing cache.rs …{/}
{dm}└─{/} {r}◐{/} verify {r bd}needs input{/} {dm}approve test run?{/}
Per-sub-agent status + colour + a fold toggle for completed ones is the near-term build (Feature 1 of the resourcing roadmap); mocked as the target UX.
Compose a multi-line prompt
C-c A e
A dedicated compose buffer opens — write freely, then submit.
{dm}─ *compose: feature/token-cache* ──────────────{/}
Refactor the token cache to expire entries lazily
on read instead of a background sweep. Keep the
public API stable; add a test for the 410 path.
{dm}────────────────────────────────────────────────{/}
{ac}C-c C-c{/} submit {dm}C-c C-k cancel M-p/M-n history M-r search{/}
C-c C-c sends it; M-p/M-n walk prompt history, M-r searches it (consult).
Reviewing & shipping
Review a PR
C-c A c→r
From a Requests row (or the quick-action), launch a review session pre-linked to the PR.
{hd}Requests (3){/}
{ac}>{/} {c}⎇ {/} {y}2d{/} upside{gd}#16570{/} {g}●{/}{gd bd}@{/} {gd}CSE-201: refactor token cache{/}
{dm}review session launching…{/} {ac}◉{/}
{dm bd}↓ {/}{r}15d{/} upside#16568 {bo}π{/} {dm}bump: dependency update{/}
The ◉ marks the row now has a live review session. C-c s l links another PR to the current session; C-c s u unlinks.
Inspect · tag · link
C-c s i
C-c s i prints a one-line session summary; C-c s t a adds a tag; C-c s l
links a PR.
{dm}Session:{/} 3f9a1c2e… {dm}Conv:{/} b71e… {dm}Model:{/} {g}opus{/} {dm}Mode:{/} {y}auto{/}
{dm}Tags:{/} {c}#token-cache #CONN-473{/} {dm}Exchanges:{/} 14
{dm}Workspace:{/} ~/tools/decknix
{dm}Created:{/} 2026-07-08 09:12 {dm}Modified:{/} 2026-07-08 13:47
Tags, workspace, model, permission-mode and linked PRs all persist per-conversation and survive resume/fork.
Copy a region as Slack / HTML / PDF
C-c x s
Select any Markdown in the buffer and export it — s Slack mrkdwn, h HTML,
P PDF, t plain / re-align table.
{dm}region (Markdown):{/}
**Root cause:** the `/rest/api/2/search` endpoint was removed.
See [the changelog](https://x/CHANGE-20).
{ac}C-c x s{/} {dm}→ clipboard (Slack mrkdwn):{/}
{g}*Root cause:* the `/rest/api/2/search` endpoint was removed.
{/}
Fence- and table-aware, pure-string transforms — paste straight into Slack, a PR comment, or a doc.
Managing many sessions
Resume a past session
C-c A s
One picker spans live + saved + new. Live rows are coloured, saved ones dimmed;
M-a / M-c / M-p toggle Auggie / Claude / Pi rows, C-u C-c A s expands every
saved snapshot.
{dm}Sessions — live + saved (M-a/M-c/M-p filter · C-u expand):{/}
{ac}>{/} {g}C{/} {g}●{/} review/auth {dm}decknix · 2m{/} {c}#auth{/}
{y}A{/} {y}◐{/} feature/token-cache {dm}decknix · now{/}
{dm}C{/} {dm}·{/} CONN-473 spike {dm}decknix · 3d{/} {c}#spike{/}
{dm}A{/} {dm}·{/} proptrack timeout {dm}nurturecloud · 5d{/}
Selecting a saved row resumes it with its history window restored and input ring rebuilt. C-c A f forks a session instead — the fork keeps the source's tags, workspace and conversation identity.
Search every session
C-c A g
Full-text ripgrep across all transcripts, with the match in context. RET jumps
straight to the matching turn.
{dm}Grep all sessions:{/} {c}pubsub timeout{/}
{gd}proptrack timeout{/} {dm}nurturecloud · 5d{/}
{dm}…the {/}{y}pubsub timeout{/}{dm} was an unacked message redelivery …{/}
{gd}CONN-473 spike{/} {dm}decknix · 3d{/}
{dm}…retry policy on a {/}{y}pubsub timeout{/}{dm} should back off …{/}
Never lose a decision — find the conversation where you made it.
Jump to what needs you
C-c A j
Cycle straight to the next session carrying red/amber attention — the agent-shell answer to cmux's "which one needs me?"
{dm}Jump to attention →{/} {r}◐{/} feature/token-cache {dm}(verify: “approve test run?”){/}
Repeated presses walk the attention queue: waiting sessions first, then CI failures and review requests.
Batch one prompt to several sessions
C-c A s→C-SPC…B
Mark rows in the picker with C-SPC, then batch a single prompt to all of them —
handy for "rebase onto main" or "add the new lint" across several worktrees.
{dm}Sessions (C-SPC mark · B batch):{/}
{g}✓{/} {g}C{/} {g}●{/} review/auth {rp}decknix{/}
{g}✓{/} {g}A{/} {g}●{/} feature/token-cache {rp}decknix{/}
{dm}A{/} {dm}·{/} proptrack timeout {rp}nurturecloud{/}
{dm}2 marked{/} {ac}· B → send one prompt to both{/}
Marked sessions each receive the composed prompt; results stream back into their own buffers.
Keys used in this tour
| Keys | Does |
|---|---|
| C-c w | Open the *decknix* welcome screen |
| C-c A w | Open the Agents sidebar (workspace) |
| C-c A a / C-c A n | Start / switch · force a new session |
| C-c A s | Session picker (live + saved + new); M-a/M-c/M-p filter, C-SPC mark |
| C-c A g | Grep across every session |
| C-c A j | Jump to the next session needing attention |
| C-c A e | Compose a multi-line prompt |
| C-c A c r / l / u | Review PR · link PR · unlink |
| C-c x s / h / P / t | Copy region as Slack · HTML · PDF · plain |
| C-c s i / t a / l | Session info · add tag · link PR |
Inside an agent-shell buffer the A prefix is dropped — C-c s, C-c e, C-c ?.
Full reference: Keybindings.
Foundation (Layer 1)
The foundation layer provides the core shell infrastructure that everything else builds on.
Core Components
shell-maker
The underlying comint-like buffer management library. Handles prompt rendering, input submission, scroll behaviour, and process lifecycle. Agent Shell inherits its robust terminal semantics.
ACP (Augment Code Protocol)
The acp package implements the wire protocol between Emacs and the auggie CLI. Unlike HTTP-based integrations, ACP runs auggie as a subprocess — no server, no ports, no latency.
agent-shell.el
The main interface package. Provides:
- Agent configuration and model selection
- Session lifecycle (start, interrupt, rename)
- Mode-line status display
- Buffer management
Tiered Package Sourcing
Packages are sourced from the most stable channel available:
Priority 1: stable nixpkgs → (nothing currently — all are too new)
Priority 2: unstable nixpkgs → shell-maker, acp, agent-shell
Priority 3: custom derivations → agent-shell-manager, workspace, attention
Custom derivations use trivialBuild with pinned GitHub revisions and hashes:
agent-shell-manager-el = pkgs.emacsPackages.trivialBuild {
pname = "agent-shell-manager";
version = "0-unstable-2026-03-17";
src = pkgs.fetchFromGitHub {
owner = "jethrokuan";
repo = "agent-shell-manager";
rev = "53b73f1...";
hash = "sha256-JPB/OnOhYbM0LMirSYQhpB6hW8SAg0Ri6buU8tMP7rA=";
};
packageRequires = [ agent-shell ];
};
As packages mature into nixpkgs, they'll migrate up the priority chain automatically.
Default Behaviour
| Setting | Value | Why |
|---|---|---|
agent-shell-preferred-agent-config | 'auggie | Skip agent selection prompt |
agent-shell-session-strategy | 'new | Always start fresh; session management via our picker |
agent-shell-header-style | 'text | Model/mode in mode-line, not graphical header |
agent-shell-show-session-id | t | Show session ID for resume/history |
Welcome Message
Every new session displays a custom welcome with a quick-reference keybinding card:
Welcome to Auggie (opus4.6, agent mode)
────────────────────────────────────────────────────
Quick Reference
C-c e Compose Open multi-line prompt editor
C-c s Sessions Pick / resume / start session
C-c q Quit Save and quit session
C-c h History View conversation history
C-c t t Template Insert a prompt template
C-c c c Command Pick & insert a slash command
C-c T t Tag Tag this session
C-c T l By tag Filter sessions by tag
C-c ? Help Full keybinding reference
────────────────────────────────────────────────────
The welcome is implemented as an :override advice on agent-shell-auggie--welcome-message, preserving the original auggie welcome while appending the reference card.
Nix Options
programs.emacs.decknix.agentShell.enable = true; # Enable the entire ecosystem
Disabling this single option removes all agent-shell packages and configuration.
Model Selection
This section covers Auggie, whose model is fully driven by decknix (default, per-quickaction, and per-conversation override re-applied on resume). Claude and Pi select models differently — see Model selection by agent.
Auggie exposes several model families. The framework default is
prism-a — Augment's hybrid router that mixes Opus 4.7, Sonnet 4.6,
and Gemini Flash per turn (around 28 % cheaper than uniform Opus 4.7
on review-shaped workloads without losing depth where it matters).
Recommended Models by Task
| Task | Recommended | Why |
|---|---|---|
PR review (/review-service-pr) | prism-a | Router picks Opus on hard diffs, cheaper models on skim — best $/quality tradeoff. |
| Implementation against a defined spec | sonnet4.6 | Mechanical work doesn't need flagship reasoning; ~46 % cheaper than Opus. |
| Debugging in a familiar codebase | sonnet4.6 | Same — context is local, reasoning is bounded. |
| Architecture / planning | opus4.7 | Long-horizon reasoning, opinionated codegen — earns its 167 % credit cost. |
| Triage / classification | haiku4.5 | ~33 % credit cost; fine for short, well-bounded prompts. |
| Framework iteration (decknix) | prism-a | Varied workload — let the router pick. |
Override Levers
Three layers, narrowest wins:
-
Per-session —
C-c C-vinside any agent-shell buffer picks a model for the running conversation; persisted in~/.config/decknix/agent-sessions.jsonand re-applied on resume. This is the right lever for one-off task adjustments. The persistence works for every provider; only the resume mechanism differs — Auggie pins it at launch via--model <id>, while Claude/Pi (which take no model launch flag) replay it over ACP (session/set_model) once the resumed session reports ready. See Model selection by agent. -
Per-purpose (Nix) —
programs.emacs.decknix.agentShell.purposes.<name>.{provider,model,mode}pins the provider, model, and permission mode for every launch of a given purpose. Three purposes ship today:pr-review— human-authored PR review (C-c A c r, sidebar Requests row, batch processor). Default:provider = "claude-code",model = "sonnet",mode = "auto".bot-pr-review— PR whose author is a bot (auto-dispatched via theAauto-review toggle, or matched by author heuristic). Default:provider = "claude-code",model = "sonnet",mode = "auto"—sonnetrather than the cheapest tier because an unattendedautoreview needs a model that honoursauto.new-session— interactive / QUICKC-c A n. Default:provider = "claude-code",model = null,mode = "auto". Itsprovideralso feedsdecknix-agent-default-provider.
Example — pin PR reviews to Claude with opus for depth, route bot diffs to the cheaper haiku on the same provider, and start new sessions in
auto:programs.emacs.decknix.agentShell.purposes = { pr-review = { provider = "claude-code"; model = "opus"; mode = "auto"; }; bot-pr-review = { provider = "claude-code"; model = "haiku"; mode = "auto"; }; new-session = { provider = "claude-code"; mode = "auto"; }; };Set
model = nullto defer to the provider's own default (no--modelflag is added and no ACP replay runs). Values are validated at daemon start: an unknown provider coerces todecknix-agent-default-provider, and an unknown model drops tonil; both cases log a warning to*Warnings*. -
Framework default —
decknix.cli.auggie.settings.modelis written to~/.augment/settings.jsonand used when no--modelflag is supplied. Org and personal layers can override withlib.mkDefaultor plain assignment. Framework defaults for Claude / Pi are provider-native — see Model selection by agent.
Per-conversation overrides set via C-c C-v always win over the
purpose and framework defaults. On resume the persisted model is
re-applied automatically: for Auggie it flows through
decknix--resume-command-build and is appended to the ACP command as
--model <id>; for Claude/Pi it is replayed over ACP
(session/set_model) by decknix--agent-model-replay-on-ready after
the session reports ready.
Purposes that are not pinned via Nix (interactive C-c A n, fork,
worktree w s) keep decknix-agent-default-provider with no model
pin — they use the provider's own default until you make a
per-session choice with C-c C-v.
Permission mode
The session permission mode follows the same narrowest-wins
hierarchy as the model, on providers that expose it (today Claude,
whose modes are default, auto, acceptEdits, bypassPermissions,
and plan):
- Per-session —
C-c C-minside any agent-shell buffer switches the running conversation's mode; the choice is persisted in~/.config/decknix/agent-sessions.jsonand re-applied on both resume and fork, so a session left inautodoesn't fall back to per-command permission prompts when you return to it. - Per-purpose (Nix) —
purposes.<name>.modeseeds the mode for new launches of that purpose.new-session.mode(default"auto") is what freshC-c A nsessions start on, and it's the fallback for resume/fork when a conversation has no saved mode override. - Provider default — used when no purpose mode is set and the conversation has no override.
Providers without session modes (Auggie, Pi) ignore the mode entirely — it can never break a launch.
Multi-Session (Layer 2)
Layer 2 turns agent-shell from a single-buffer chat into a full session management system.
Unified Session Picker (C-c s)
The session picker combines three sources into one completing-read:
Agent session:
[new] Start a new auggie session
[live] *agent-shell*<proptrack-fix>
[live] *agent-shell*<decknix-docs>
[saved] a1b2c3d4 2h ago 12x Investigate proptrack pubsub timeout...
[saved] e5f6g7h8 3d ago 8x Refactor agent-shell keybindings... [decknix, refactor]
- [new] — starts a fresh agent-shell session
- [live] — switches to an existing Emacs buffer
- [saved] — resumes a saved auggie session (boots a new agent-shell with
--resume <id>)
Saved sessions show: truncated ID, relative time, exchange count, first message preview, and tags (if any).
Session Resume
When you select a saved session, the picker:
- Appends
--resume <session-id>to the ACP command - Starts a new agent-shell buffer with the auggie session restored
- Stores the auggie session ID in a buffer-local variable for history/tagging
;; The resume mechanism — auggie CLI handles the actual session restore
(let ((agent-shell-auggie-acp-command
(append agent-shell-auggie-acp-command
(list "--resume" session-id))))
(agent-shell-start :config (agent-shell-auggie-make-agent-config)))
Session History (C-c h / C-c H)
View the full conversation history for any session:
C-c h— DWIM: if in an agent-shell buffer with a known session, shows that session's history. Otherwise, prompts to pick.C-c H— Always pick: shows the session picker regardless of current buffer.
History is rendered by generating a share link via auggie session share <id> and opening it in xwidget-webkit (embedded browser) or eww (text browser) as fallback — all inside Emacs.
Clean Quit (C-c q)
Quitting a session:
- Prompts for confirmation (
y-or-n-p) - Switches to the previous buffer
- Kills the agent-shell buffer (sends SIGHUP to auggie, which auto-saves the session)
The session is immediately available in the picker's saved list for future resume.
Buffer Rename (C-c r)
Rename the agent-shell buffer for clarity:
*agent-shell* → *agent-shell*<proptrack-pubsub-fix>
Uses agent-shell-rename-buffer from the core package.
Extensions
Manager Dashboard (C-c m)
agent-shell-manager provides a tabulated list of all agent-shell buffers at the bottom of the frame. Toggle with C-c m.
Workspace Tab (C-c w)
agent-shell-workspace creates a dedicated tab-bar tab with a sidebar showing all agent sessions. Toggle with C-c w.
Attention Tracker (C-c j)
agent-shell-attention adds a mode-line indicator showing pending/busy session counts:
AS:2/1 ← 2 sessions pending input, 1 busy
C-c j jumps to the next session needing attention.
Nix Options
programs.emacs.decknix.agentShell = {
manager.enable = true; # Tabulated dashboard
workspace.enable = true; # Tab-bar workspace
attention.enable = true; # Mode-line tracker
};
Productivity (Layer 3)
Layer 3 provides the tools for structured, repeatable interactions with the AI agent.
Compose Buffer (C-c e)
A magit-style multi-line editor for writing prompts. Opens at the bottom of the frame:
┌─────────────────────────────────────────────┐
│ *agent-shell*<my-session> │
│ │
│ ... conversation ... │
│ │
├─────────────────────────────────────────────┤
│ Compose prompt → C-c C-c submit, C-c C-k │
│ │
│ Please refactor the UnrecoverableError │
│ handler to support a new category: │
│ │
│ - Add `TransientNetworkError` subclass │
│ - Wire it into the metrics recorder │
│ - Update the Terraform alert definitions │
│ │
└─────────────────────────────────────────────┘
C-c C-c— submit the composed text to the agentC-c C-k— cancel and close the compose bufferC-c C-s— toggle sticky (stays open after submit) vs transient (closes after submit)C-c k k— interrupt the agent,C-c k C-c— interrupt and submit- Full text-mode editing:
RETfor newlines, no accidental submissions
Prompt History (M-p / M-n)
The compose buffer supports prompt history across all sessions — cycle through previously sent prompts:
| Key | Action |
|---|---|
M-p | Previous prompt (older) |
M-n | Next prompt (newer) |
M-r | Search all prompts (consult fuzzy match) |
History is loaded lazily from auggie session files — M-p starts with the current session, then progressively loads older sessions as you keep pressing. M-r provides a full fuzzy search across all sessions using consult.
Your current input is saved when you start navigating and restored when you cycle past the newest entry.
Yasnippet Prompt Templates (C-c t t)
Seven built-in templates with interactive fields:
| Template | Key | Purpose |
|---|---|---|
/review | C-c t t → review | Code review with focus selector (bugs, performance, security, readability) |
/refactor | C-c t t → refactor | Refactoring with pattern selector (extract, rename, DRY up, etc.) |
/test | C-c t t → test | Test generation covering happy path, edge cases, errors |
/explain | C-c t t → explain | Code explanation with aspect focus |
/fix | C-c t t → fix | Bug fix with stack trace placeholder |
/implement | C-c t t → implement | Feature implementation following existing patterns |
/debug | C-c t t → debug | Debugging with logs and steps-taken fields |
Templates auto-populate the current file from the source buffer. TAB advances between fields; yas-choose-value provides dropdown selectors.
Creating Templates
C-c t n— create a new yasnippet templateC-c t e— edit an existing template
Templates are stored in ~/.emacs.d/snippets/agent-shell-mode/.
Custom Slash Commands (C-c c c)
Slash commands are markdown files with YAML frontmatter, deployed to ~/.claude/commands/ (the shared location read by both Claude Code and Auggie) and fanned out to ~/.pi/agent/prompts/ so Pi picks them up as /name prompt templates too:
---
description: Create a new session from a Jira ticket key
argument-hint: [session name or JIRA-KEY]
---
Create a new Augment session and rename it in one step.
...
Built-in Commands
| Command | Description |
|---|---|
/start | Create a new session from a Jira ticket key or plain name |
/find-session | Search all saved sessions by keyword (up to 500) |
/pivot-conversation | Hard pivot — discard current plan, re-evaluate |
/step-back | Stop, summarise progress, wait for direction |
Command Discovery
The picker (C-c c c) scopes discovery to the agent the current session is running as: a Claude/Auggie session lists ~/.claude/commands/ (plus project-level .claude/commands/ and the legacy ~/.augment/commands/ during the transition), while a Pi session lists ~/.pi/agent/prompts/. Invoked outside an agent session, it falls back to scanning the union so everything is still discoverable. Duplicates collapse (preferring the canonical global copy), and each command shows its scope and description:
Command:
/start (global) Create a new session from a Jira ticket key
/find-session (global) Search all saved sessions by keyword
/pivot-conversation (global) Hard pivot — discard current plan
/deploy-check (project) Verify deployment prerequisites
Nix-Managed vs Runtime Commands
Nix-managed commands are deployed as symlinks. User-created commands are regular files. On decknix switch, Nix refreshes its symlinks; runtime-created commands persist untouched.
Session Tagging (C-c T)
Tag sessions with freeform labels for organisation:
| Key | Action |
|---|---|
C-c T t | Add a tag (with completion from existing tags) |
C-c T r | Remove a tag |
C-c T l | Filter sessions by tag → resume picker |
C-c T e | Rename a tag across all sessions |
C-c T d | Delete a tag globally |
C-c T c | Cleanup orphaned tags (sessions that no longer exist) |
Tags are stored in ~/.config/decknix/agent-sessions.json, keyed by auggie session ID. The session picker shows tags inline:
[saved] a1b2c3d4 2h ago 12x Fix pubsub timeout... [proptrack, bug]
Quick Actions
PR Review (C-c c r / C-c A c r)
Start a code review session for a GitHub PR:
- Prompts for PR URL (auto-detects from clipboard)
- Creates a named session:
Review: owner/repo#123 - Tags the session with
reviewand sends/review-service-pr <url>
Batch Process (C-c c B / C-c A c B)
Launch multiple sessions from a single editor — ideal for batch code reviews or parallel investigations:
┌─────────────────────────────────────────────┐
│ Batch: C-c C-c submit | C-c C-k cancel │
│ │
│ # Batch session launcher — workspace: ~/.. │
│ --- backend : ~/Code/my-project │
│ https://github.com/org/api/pull/42 │
│ https://github.com/org/api/pull/43 │
│ │
│ --- frontend │
│ https://github.com/org/web/pull/17 │
│ │
└─────────────────────────────────────────────┘
Syntax:
--- <name> [: <workspace>]— group header (items below launch as one session)- Ungrouped URLs — each gets its own session
#lines — comments, ignored
Snippets (via yasnippet in batch compose mode):
| Key | Snippet | Description |
|---|---|---|
--- | Group header | --- <name> : <workspace> with tab stops |
pr | PR URL | Generic github.com/<owner>/<repo>/pull/<number> |
Org-specific snippets (e.g., pre-filled workspace paths) can be added via downstream decknix-config.
C-c C-c parses the buffer and launches all sessions. A summary buffer shows success/failure for each.
Auto-Review (T → Requests → A)
Automatically dispatch a review session when a PR review request that @-mentions you arrives in the hub Requests section — so a verdict is waiting by the time you switch to it.
Cycle the mode from the sidebar Toggles transient (T → Requests → A):
| State | Behaviour |
|---|---|
off | Disabled (default). |
bot+@ | Auto-review bot-authored PRs that @-mention you, via /review-and-ship-bot-pr. |
human+@ | Auto-review human-authored PRs that @-mention you, via the background /review-service-pr-factory. |
any+@ | Both — bots ship, humans get the background review. |
Every active state requires the @-mention: this is a deliberate safety
guard so team-noise PRs (where you are not directly addressed) never spawn
a session. Human reviews run as a background factory session whose verdict
surfaces through the attention indicator (AS:n/m, jump with C-c j).
Incoming-only: turning the toggle on seeds the current backlog as already-handled, so only genuinely new mentioned PRs dispatch — enabling it never floods you with sessions for the existing queue. A live-session guard plus a per-PR dedup set prevent double-dispatch.
Per-workspace commands: override the slash command per repository
workspace via decknix-auto-review-commands (an alist of
(workspace . (:review CMD :ship CMD))); the global defaults are
decknix-auto-review-default-review-command and
decknix-auto-review-default-ship-command.
Focus Steal (T → Global → g, default off)
Off by default, Emacs never steals window focus. Opt in to have the frame raise itself to the foreground when work needs your attention — handy when agent sessions run in the background behind other apps.
Cycle the mode from the sidebar Toggles transient (T → Global → g, or
the footer focus label):
| State | Behaviour |
|---|---|
off | Never raise the frame (default). |
attention | Raise the frame when a backgrounded session enters a waiting / needs-input state. |
both | Also raise the frame when a new session is created (e.g. an auto-review dispatch). |
The attention raise is edge-triggered (once per transition into waiting)
and skipped when you are already viewing that session. On the background
Emacs daemon an osascript … activate is issued so macOS actually brings
the app forward.
Navigation: C-c A o jumps to the sidebar window from anywhere;
C-c A j jumps the other way, to the next session needing input. These
cover session↔sidebar movement whether or not focus-steal is enabled.
Priority View (C-c A p, opt-in)
A ranked, lane-based "what should I do next?" view over the hub's existing
data, shown in a standalone read-only *Agent Priority* buffer. Off by
default; enable it with
programs.emacs.decknix.agentShell.hub.priority.enable = true to bind
C-c A p.
Four ordered lanes, highest-leverage first:
| Lane | What it surfaces |
|---|---|
| Discussions | PRs where a human is awaiting your reply (highest priority). |
| Reviews | PRs awaiting your review verdict. |
| Tasks | Your non-done Jira issues. |
| Queue | Your open WIP PRs flowing through the pipeline. |
Within each lane, items you were directly @-mentioned on rank first, then
oldest first. In the buffer: RET opens the item, g refreshes, q
quits.
This is phase 1 (existing sources only). A sidebar live-view mode, pin / exclude support, and generic GitHub Actions pipeline enrichment of the Queue lane are tracked in issues #142 and #141.
Formatting Output
The agent emits raw markdown, which the comint buffer shows literally —
collapsed tables and **bold** / ### head / [t](u) syntax that pastes
badly into Slack and other tools. Three complementary tools address this.
Auto-aligned Tables (default on)
Tables in agent-shell output are automatically aligned via display
overlays — the columns line up visually while the underlying buffer
text stays raw markdown, so M-w and Copy Region As… still see the
original. When an aligned table would be wider than the window it reflows
into the bullet list shown below instead. The same overlay is enabled in
inline-review buffers. Disable with
programs.emacs.decknix.agentShell.tableOverlay.enable = false; to fall
back to the on-demand command only.
Reformat Table (C-c x → t)
Re-aligns the GFM table at point (or in the active region) so the pipes line up:
| Name | Age | City |
| ----- | --- | ---- |
| Alice | 30 | NYC |
When the aligned table would be wider than the window, it reflows into a per-row bullet list with key/value sub-items instead — readable even in a narrow sidebar:
• Alice
- Age: 30
- City: NYC
Copy Region As… (C-c x)
Select a region, then C-c x opens a transient that converts it and puts
the result on the kill-ring in the chosen syntax:
| Key | Format | Notes |
|---|---|---|
m | Markdown | tables re-aligned, prose untouched |
s | Slack mrkdwn | *bold*, _italic_, ~strike~, <url|text>, headings → bold line, &/</> escaped, tables → aligned code block |
h | HTML | via pandoc (GFM → HTML) |
p | Plain text | emphasis stripped, links → text (url), tables aligned |
It also has an Export region to file entry that writes a file rather than copying to the kill-ring:
| Key | Format | Notes |
|---|---|---|
P | via pandoc; prompts for a path, then offers to open it |
Both the HTML and PDF paths need pandoc, and PDF additionally needs a
PDF engine on PATH. decknix installs both by default — pandoc plus
typst (small, fast, no TeX) — so C-c x h / C-c x P work out of the
box after decknix switch. See the Nix options below to change the
engine or opt out. If no engine is found the command reports which to
install (the auto-detect order is typst, tectonic, weasyprint,
wkhtmltopdf, xelatex, pdflatex) rather than failing silently.
C-c x is bound in agent-shell buffers and in markdown / review buffers
(C-c y is reserved for yasnippet). The Slack mapping follows
Slack's mrkdwn spec.
Switching Agents / Migrating off Auggie
decknix supports three agent providers — Auggie (A), Claude (C), and
Pi (P) — and new sessions default to Claude. There are three
distinct operations, and it's worth knowing which one to reach for:
| Goal | Command | Crosses agents? |
|---|---|---|
| Continue the same conversation on the same agent | Resume — picker C-c A s (Previous / Saved), or restart C-c s R | No |
| Continue a discussion on a different agent (Auggie → Claude / Pi) | Fork — C-c A f / C-c s f | Yes |
| Change model mid-conversation (same agent) | C-c C-v | No |
| Change permission mode mid-conversation (same agent) | C-c C-m | No |
There is no "clone" command — fork is the cross-agent path.
Why resume can't switch agents
Resume is provider-native: each agent stores its transcripts in its own
directory and format, and resume relaunches that agent with --resume <id>:
- Auggie →
~/.augment/sessions/*.json - Claude →
~/.claude/projects/*.jsonl - Pi →
~/.pi/sessions/
The picker reads the session metadata and always restores the original provider, so an Auggie transcript can't be replayed as Claude.
Fork hands off context to the new agent
C-c A f (decknix-agent-session-fork) is the tool for "I'm moving this
work off Auggie":
- Prompts for the new provider — pick Claude or Pi.
- Pre-seeds the source session's workspace and tags (editable before you confirm).
- Auto-sends a context hand-off as the new session's first message, naming the source provider, session id, and best-effort transcript path — so the new agent can read the prior conversation and pick up where you left off.
This is best-effort continuity (the new agent reads the named transcript file to reload context rather than a true session port), but for "continue this discussion in Claude" it's the intended button. Invoked outside an agent-shell buffer there's no source, so fork degrades to a plain new session.
Model selection by agent
C-c C-v (set session model) isn't Auggie-only. It's the upstream
agent-shell verb that lists whatever models the running agent's ACP
bridge advertises and switches the live session to your choice via an
ACP session/set_model request. decknix persists that choice against
the conversation for every provider, and restores it on resume
— the only difference is the mechanism:
- Auggie pins the model at launch via its
--model <id>flag, so the resumed conversation comes up on the right model immediately. - Claude / Pi don't accept a model launch flag, so decknix instead
replays the saved model over ACP (
session/set_model) the moment the resumed session reports ready — the same leverC-c C-vuses live. The result is the same: your per-conversation model survives the resume.
| Agent | Switch mid-session | Restored on resume? | Set the default |
|---|---|---|---|
| Auggie | C-c C-v | ✅ yes — --model at launch | decknix.cli.auggie.settings.model |
| Claude | C-c C-v | ✅ yes — ACP set_model replay | agent-shell-anthropic-default-model-id, or ANTHROPIC_MODEL env |
| Pi | C-c C-v if Pi's bridge advertises models | ✅ yes — ACP set_model replay | Pi's own config (~/.pi.json) |
The default still matters for the first turn of a brand-new
conversation, before you've made any C-c C-v choice to persist — set
it so fresh sessions start on the right model.
Automated purposes — for PR reviews (C-c A c r, sidebar Requests
row, auto-review dispatch), pin both the provider and model per
purpose via
programs.emacs.decknix.agentShell.purposes.<name>.{provider,model}
in your Nix config. See Per-Purpose Provider &
Model for the full
list of purposes, defaults, and validation semantics. Purpose pins
survive across all three providers — the model rides --model on
launch for Auggie and is replayed over ACP for Claude / Pi.
Claude — set the per-Emacs default in your personal config so every
new Claude session starts on the right model (after that, C-c C-v
choices persist and are replayed on resume):
(with-eval-after-load 'agent-shell-anthropic
(setq agent-shell-anthropic-default-model-id "claude-sonnet-4-5"))
To point at a custom or proxy endpoint/model instead, use the environment:
(setq agent-shell-anthropic-claude-environment
(agent-shell-make-environment-variables
"ANTHROPIC_MODEL" "..."))
Pi — the default model is governed by Pi itself (its own config /
in-session controls); decknix wires no default-model-id for Pi. But
once you pick a model with C-c C-v it's persisted and replayed on
resume like Claude. If C-c C-v reports "No session models
available", the Pi ACP bridge isn't advertising a model list — there's
nothing to switch or persist, so select the model through Pi's own
configuration instead.
Permission mode selection
C-c C-m (set session mode) is the permission-mode analogue of
C-c C-v. It lists whatever modes the running agent advertises and
switches the live session over ACP. Today only Claude exposes
session modes — default (prompt each time), auto (Claude
auto-approves tool use, including shell commands, via its own
classifier), acceptEdits (auto-accept file edits, still prompts for
commands), bypassPermissions (no prompts — unsafe), and plan
(read-only). Providers without session modes (Auggie, Pi) ignore it.
decknix persists your choice against the conversation and re-applies
it on both resume and fork, so a session you switched to auto stays
on auto when you come back to it — no more re-approving commands after
every resume. A fork inherits its parent conversation's mode. When a
conversation has no saved mode override, resume/fork fall back to the
new-session purpose default (below).
The default for brand-new sessions is the new-session purpose's
mode, which ships as "auto". Override it (or the per-review-purpose
modes) in Nix:
programs.emacs.decknix.agentShell.purposes = {
new-session = { mode = "auto"; }; # default for C-c A n
pr-review = { mode = "auto"; };
bot-pr-review = { mode = "auto"; };
};
See Per-Purpose Provider &
Model for the full
purpose list and validation semantics (an unknown mode, or one for a
provider without session modes, drops to nil at boot with a warning).
Notes
- Start fresh on Claude with plain
C-c A n(it prompts for provider;C-u C-c A nskips the prompt and uses the default, Claude).
Nix Options
programs.emacs.decknix.agentShell = {
templates.enable = true; # Yasnippet prompt templates
commands.enable = true; # Nix-managed slash commands
hub.priority.enable = false; # opt-in Priority view (C-c A p)
tableOverlay.enable = true; # auto-align GFM tables via display overlays
# Copy-as-format / export runtime deps (C-c x h / C-c x P)
copyRegion.pandoc.enable = true; # install pandoc (HTML + PDF)
copyRegion.pdfEngine = "typst"; # "typst"|"tectonic"|"weasyprint"|"wkhtmltopdf"|null
};
programs.emacs.decknix.ui.focus.steal = "off"; # "off" | "attention" | "both"
Integration (Layer 4)
Layer 4 connects the agent shell to external tools and services via the Model Context Protocol (MCP).
MCP Server Configuration
MCP servers extend the agent's capabilities by providing access to external data sources and APIs. Decknix manages them declaratively:
{ ... }: {
decknix.cli.auggie.mcpServers = {
context7 = {
type = "stdio";
command = "npx";
args = [ "-y" "@upstash/context7-mcp@latest" ];
};
"gcp-monitoring" = {
type = "stdio";
command = "npx";
args = [ "-y" "gcp-monitoring-mcp" ];
env.GOOGLE_APPLICATION_CREDENTIALS = "~/.config/gcloud/credentials.json";
};
"nurturecloud-knowledge-base" = {
type = "stdio";
command = "npx";
args = [ "-y" "nurturecloud-kb-mcp" ];
};
};
}
Viewing Servers (C-c A S)
The MCP server listing shows all configured servers in a formatted buffer:
MCP Server Configuration
════════════════════════════════════════════════════════
Source: ~/.augment/settings.json
context7
type: stdio
command: npx
args: -y @upstash/context7-mcp@latest
gcp-monitoring
type: stdio
command: npx
args: -y gcp-monitoring-mcp
env:
GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/credentials.json
════════════════════════════════════════════════════════
Runtime changes (auggie mcp add) are temporary.
To persist, edit Nix config and run decknix switch.
Press q to close this buffer.
Declarative vs Runtime
The two-tier model:
- Nix-managed (persistent) — defined in your Nix config, deployed on
decknix switch. This is the baseline. - Runtime (temporary) — added via
auggie mcp addduring a session. Lost on nextdecknix switch.
This lets you experiment with new MCP servers without committing to them, while ensuring your team's standard servers are always present.
How MCP Enhances the Agent
With MCP servers configured, the agent can:
| Server | Capability |
|---|---|
context7 | Query up-to-date library documentation |
gcp-monitoring | Search GCP logs, error groups, Datastore entities |
nurturecloud-knowledge-base | Search resolved Jira tickets and internal docs |
jira | Read/create/transition Jira issues |
confluence | Search and create Confluence pages |
github | Full GitHub API access (issues, PRs, code search) |
The agent automatically discovers available MCP servers and uses them when relevant to the conversation.
Organisation-Specific Servers
Org configs can layer additional MCP servers on top of the framework defaults. See Organisation Configs for how this works.
For NurtureCloud-specific MCP server configuration, see the NC Agent Shell Workflows documentation.
Context Awareness (Layer 5)
Layer 5 makes the agent shell work-aware — it passively tracks the issues, PRs, CI status, and review threads relevant to your current conversation.
How It Works
┌─────────────────────────────────────────────────────────────┐
│ Issues: #51 #52 | PR: #50 | CI: ✅ | Reviews: 2 unresolved │
├─────────────────────────────────────────────────────────────┤
│ *agent-shell*<cherries-epic> │
│ │
│ Let's work on the migration wizard (#52). The PR (#50) │
│ is passing CI now. There are 2 unresolved review threads. │
│ │
└─────────────────────────────────────────────────────────────┘
The header-line at the top of every agent-shell buffer shows a live summary of tracked context.
Auto-Detection
The context panel scans buffer text for references:
| Pattern | Detected As | Example |
|---|---|---|
#123 | GitHub issue/PR (current repo) | #51 |
org/repo#123 | GitHub issue/PR (specific repo) | ldeck/decknix#52 |
PROJ-1234 | Jira ticket | ALR-4268, NC-7801 |
False positives are filtered: HTTP-200, SHA-256, UTF-8, ISO-8601 are excluded.
Data Fetching
For each detected GitHub reference, the panel fetches metadata via gh CLI:
- Issues/PRs: number, title, state (open/closed/merged), URL, type (issue vs PR)
- CI: latest run status for the current branch (pass/fail/running)
- Reviews: unresolved review thread count across open PRs in context
CI status auto-polls every 60 seconds.
Header-Line Indicators
| Indicator | Meaning |
|---|---|
Issues: #51 #52 | Tracked issues (green = open, grey = closed) |
PR: #50 | Tracked PRs (green = open, purple = merged, red = closed) |
CI: ✅ | Latest CI run passed |
CI: ❌ | Latest CI run failed |
CI: 🔄 | CI run in progress |
Reviews: 2 unresolved | Unresolved PR review threads (yellow warning) |
Context Panel (C-c I)
The full detail panel shows everything in a formatted buffer:
Agent Context Panel
────────────────────────────────────────────────────
Issues
────────────────────────────────────────
#51 Cherries epic — high-appeal features open
#52 Migration wizard open 📌
Pull Requests
────────────────────────────────────────
🟢 #50 Agent shell context awareness open
Branch & CI
────────────────────────────────────────
Branch: feature/context-panel (ldeck/decknix)
CI: ✅ success CI / Build and Test
Reviews
────────────────────────────────────────
4 threads, 2 unresolved
────────────────────────────────────────────────────
Press q to close. C-c i g to open item in browser.
Pin / Unpin
Manually pin items to keep them in context even if they're not mentioned in the conversation:
| Key | Action |
|---|---|
C-c i a | Pin an issue/PR (e.g., #49, NC-1234, org/repo#12) |
C-c i d | Unpin — remove from tracked context |
Pinned items are marked with 📌 in the detail panel.
Navigation
| Key | Action |
|---|---|
C-c i i | List tracked issues (completing-read → open in browser) |
C-c i p | List tracked PRs |
C-c i c | Refresh and show CI status |
C-c i r | Refresh and show review thread count |
C-c i g | Open any tracked item in external browser |
C-c i f | Visit in magit-forge |
Persistence
Pinned context items are saved per-session in ~/.config/decknix/agent-sessions.json (the same file used for tags). When you resume a session, pinned items are restored and metadata is re-fetched.
Nix Options
programs.emacs.decknix.agentShell.context.enable = true;
Requires gh CLI on $PATH (included in decknix default packages).
On-Support (DoS) Features
The agent-shell ships three in-editor surfaces that turn a team's "developer-on-support" (DoS) playbook from a static page you read into a live environment you run: a prioritised board, a filterable dashboard, and a guided checklist. They give a support engineer magit-like single-key actions over the live worklist — browse, investigate, spawn an agent, export the day's audit — without leaving Emacs.
Org engine required. These surfaces are framework code, but the data and the actions come from an org-provided CLI (the "engine") that reads your issue tracker and computes the runbook priority order. NurtureCloud ships one as
nc-dos(see the org config's TechOps On-Support page). Point thedecknix-dos-board-cli/decknix-support-dashboard-*variables at your own engine to light these up for another org.
The three surfaces (complementary)
| Surface | Command | What it is | Reach for it to… |
|---|---|---|---|
| Priority board | C-c A B | The cockpit — the runbook's ranked worklist (incidents → alerts → tasks) with one-key actions per row | Work top-down: land on the next item and act |
| Support dashboard | C-c A D | The reference view — the full board + alert feed, grouped by status and filterable | Survey / slice by user, category, or service; draft the daily log |
| Guided workflow | C-c A W | The map — the day-aware "what to do, when, how" checklist | Know the order of the day and tick tasks off |
All three are read-only, deterministic (no LLM), and refresh only while visible, so they are cheap to leave open all day. They are backed by the same engine, so the board here and the terminal console stay in parity.
The DoS priority board — C-c A B
The living playbook. It renders the engine's computed priority model as ranked lanes and stamps each row with its ticket key, so single-key actions operate on the item at point. The header is the constant-attention surface: weekday, deploy/freeze posture, weekly-report freshness, and live open-work counts.
| Key | Action |
|---|---|
RET / o | Browse the ticket at point |
i | Spawn a foreground agent (a new agent-shell session, runbook-primed) on the item |
x | Spawn a background agent (headless, logged to the engine's runs dir) |
c | Copy the exact fg/bg spawn commands for the item to the kill-ring |
n / p | Move to the next / previous item (TAB / S-TAB also) |
r | Open the current weekly report |
W | Export today's support worksheet (live counts) and open it in Emacs |
g | Refresh now |
? / . | Action menu (magit-style transient) |
q | Bury the board |
The support dashboard — C-c A D
The survey view: the DoS board grouped by status alongside the alert feed, with filtering. Best when you need to slice the board a different way (everything one person owns, one service, one category) or draft the day's report entry.
| Key | Action |
|---|---|
b / RET | Browse the issue at point |
/ | Filter by status / category / user / service |
\ | Clear all filters |
a | Assign the issue at point |
i | Investigate with a foreground agent |
A | Investigate as an alert (alert-specific prompt + pre-comment gate) |
p | Show the engine's priority panel (text) |
x | Spawn a background agent on the issue |
c | Print / copy the spawn command |
t | Toggle auto-spawn of background agents |
r | Open the weekly report |
R | Draft today's daily-log entry from the live board |
w | Open the guided workflow |
g | Refresh |
? / . | Action menu |
q | Bury |
The guided workflow — C-c A W
The day-aware checklist distilled from the playbook work order (daily checks, scheduled deploys, audits, then the priority ladder). It cross-links into the other surfaces so you can jump from a step to the live view.
| Key | Action |
|---|---|
RET | Run the action for the step at point (e.g. open service dashboards) |
o | Open the link for the step at point |
SPC | Toggle the step done |
n / p | Next / previous step |
a | Jump to alert triage (dashboard + the tracker's alert swimlane) |
b | Open the tracker's DoS board in the browser |
d | Open the support dashboard (C-c A D) |
P | Open the playbook page |
g | Refresh |
? | Action menu |
Step-by-step: a support day
- Start with the map.
C-c A W— read the day's order: production incidents first, then daily checks, scheduled deploys (on the stipulated days), audits, then the work ladder. Tick items withSPCas you go. - Do the daily checks. From the workflow,
RETon Service Health opens the service dashboards; run Build Health per your playbook. Note anything found for the report. - Live in the cockpit.
C-c A B— the board draws the ranked worklist. Work top-down:- Land on the top incident →
RETto open it; incidents preempt everything. - Move to an alert (
n) →ito open a runbook-primed agent-shell session that triages it (respecting the pre-comment gate), orxto run it in the background. - Pick up a DoS task the same way;
cfirst if you want to see/copy the exact command.
- Land on the top incident →
- Slice when needed.
C-c A D— filter (/) by user, category, or service to reconcile the full board, or draft the daily log withR. - Export the audit. Back on the board,
Wwrites today's worksheet (seeded with live counts) and opens it in Emacs — review it and paste the entry into the weekly report (ropens the report). The report is an export surface for the day's work, kept current as you go. - Parity outside Emacs. The same engine runs in a plain terminal for teammates who don't use Emacs — see From the terminal below.
From the terminal (CLI parity)
Every board action is delegated to a CLI, so the whole flow is available without Emacs. Two CLIs are relevant:
-
The org priority engine (org-provided; NurtureCloud ships
nc-dos):nc-dos-sidebaris the single-key priority console the board renders,nc-dos-sidebar --onceprints the panel,--jsonfeeds the board, andnc-dos-worksheetexports the day's worksheet. See your org config's TechOps On-Support page for the full command set. -
decknix session— the framework's workspace- and tag-aware session CLI. The agents the board spawns on a ticket land as ordinary sessions, so you can list, resume, tag, or start them from the terminal and they stay in sync with the sidebar:# Resume the latest session you spawned on a DoS item (by tag) decknix session resume --tag dos # Start a session on a ticket, seeded and pre-tagged decknix session new --tag dos -- "Investigate ALR-5752" # List Claude sessions touched today, as JSON decknix session list --agent claude --since 1d --json
How it fits together
There is one engine and several faces. The engine (org-provided) reads the tracker, computes the runbook priority order, and owns every write (agent spawns, tab opens, tick state). The board and dashboard render that model and delegate actions back to it, so the Emacs and terminal experiences never drift apart and no runbook logic is duplicated in Elisp.
Keybindings Reference
All agent-shell keybindings are available in two forms:
- In-buffer:
C-c <key>— short form, only inside agent-shell buffers - Global:
C-c A <key>— works from any buffer
The C-c A prefix is labelled "Agent" in which-key.
Session Management
| In-buffer | Global | Action |
|---|---|---|
C-c s | C-c A s | Session picker (live + saved + new) |
| — | C-c A g | Grep sessions (full-text search across all history) |
C-c q | C-c A q | Quit session (saves automatically) |
C-c h | C-c A h | View history (current session or pick) |
C-c H | C-c A H | View history (always pick) |
C-c r | C-c A r | Rename buffer |
| — | C-c A a | Start / switch to agent |
| — | C-c A n | Force new session |
| — | C-c A k | Interrupt agent |
C-c b | C-c A b | Switch agent buffer (live only) — MRU order, status-coloured |
In-Picker Keys
Every session-facing picker (C-c A s, C-c A b, C-c A g) prefixes
each row with a provider glyph — A Auggie, C Claude, P Pi —
and shares a set of picker-local action keys:
| Key | Action |
|---|---|
M-a / M-c / M-p | Toggle visibility of Auggie / Claude / Pi rows (filter is shared across all three pickers; not persisted) |
M-w | Toggle workspace filter (all workspaces ↔ the calling buffer's workspace); active filter shows in the prompt as [~/path/to/ws] |
C-SPC | Mark row for batch action |
C-k | Kill highlighted live session buffer(s) |
C-d | Delete saved / previous session from disk and metadata |
C-u | Expand (per-picker; e.g. C-u C-c A s shows every saved snapshot instead of one-per-conversation) |
Input & Editing
| Key | Action |
|---|---|
C-c e / C-c A e | Compose buffer (multi-line editor) |
RET | Send prompt (at end of input) |
S-RET | Insert newline in prompt |
C-c C-c | Interrupt running agent |
C-c E | Interrupt agent and open compose buffer |
TAB | Expand yasnippet template |
In Compose Buffer
| Key | Action |
|---|---|
C-c C-c | Submit composed prompt |
C-c C-k | Cancel / close compose buffer |
C-c C-s | Toggle sticky (stays open) vs transient |
C-c k k | Interrupt agent |
C-c k C-c | Interrupt agent and submit |
M-p | Previous prompt (history) |
M-n | Next prompt (history) |
M-r | Search prompt history (consult) |
Templates (C-c Y / C-c A t)
In-buffer, snippet insertion is handled by the upstream C-c Y ("+snippet")
prefix — no decknix-specific in-buffer binding. The agent-namespaced
C-c A t global prefix is preserved for explicit, namespaced access.
| Key | Action |
|---|---|
C-c Y | Snippet prefix (upstream) — insert / new / visit |
C-c A t t | Insert a prompt template |
C-c A t n | Create new template |
C-c A t e | Edit existing template |
Commands (C-c c / C-c A c)
| Key | Action |
|---|---|
c | Pick & insert a slash command |
n | Create new command |
e | Edit existing command |
r | Review PR by URL (quick action; launches in the pr-review purpose's auto mode) |
B | Batch process (multi-session launcher) |
l | Link PR to session |
L | Link repo+branch to session (direct-push repos) |
u | Unlink PR or repo (single picker) |
Tags
Conversation-scoped tags (add / remove / list for this session) are now
nested under the session sub-prefix at C-c s t. Global tags
(rename / delete / cleanup across all sessions) remain at C-c A T.
Conversation-scoped (C-c s t)
| Key | Action |
|---|---|
a | Add tag (create or select) |
r | Remove tag |
l | List this session's tags |
Global (C-c A T)
| Key | Action |
|---|---|
t | Tag current session |
r | Remove tag |
l | List / filter by tag |
e | Rename a tag |
d | Delete tag globally |
c | Cleanup orphaned tags |
Sidebar Actions (C-c W)
Trigger sidebar transients without switching focus away from the
agent-shell buffer. C-c W opens decknix-sidebar-transient — the
same parent menu that the sidebar's ? / h opens — exposing
Navigate / Quick / Actions plus T for the toggles sub-transient.
| Key | Action |
|---|---|
C-c W | Open sidebar action transient |
C-c W T | Toggles transient (filters, sort, indicators) |
C-c w | Toggle the workspace tab itself (unchanged) |
Model & Mode
| In-buffer | Global | Action |
|---|---|---|
C-c C-v | — | Pick model (persists per-conversation; survives resume) |
C-c C-m | — | Pick permission mode (persists per-conversation; survives resume/fork) |
See Model Selection for the
recommended-model-by-task table and the per-purpose
(programs.emacs.decknix.agentShell.purposes) / framework
(decknix.cli.auggie.settings.model) override levers.
Context (C-c i / C-c A i)
| Key | Action |
|---|---|
i | List tracked issues |
p | List tracked PRs |
c | Show CI status |
r | Show review threads |
a | Pin issue/PR to context |
d | Unpin from context |
g | Open in browser |
f | Visit in forge |
| In-buffer | Global | Action |
|---|---|---|
C-c I | C-c A I | Full context panel |
Extensions
| In-buffer | Global | Action |
|---|---|---|
C-c m | C-c A m | Manager dashboard toggle |
C-c w | C-c A w | Workspace tab toggle |
C-c j | C-c A j | Jump to session needing attention |
| — | C-c A S | MCP server list |
Help
| In-buffer | Global | Action |
|---|---|---|
C-c ? | C-c A ? | Full keybinding reference (this page, in Emacs) |
How It Compares
The Agent Shell is not another terminal multiplexer, IDE, or cloud agent — it is an editor-native coordination framework. This page is a candid assessment, not a scorecard: it compares the Agent Shell with the tools teams reach for when running AI coding agents to find (a) where those tools have better ideas worth borrowing, (b) where decknix genuinely leads, and (c) the gaps nobody fills yet that decknix is positioned to bridge. They solve overlapping problems on very different substrates.
Legend: ✅ native / first-class · ◐ partial or via workflow · ✗ absent
Competitor features move quickly. This snapshot was compiled July 2026; verify specifics against each tool's current docs (linked below).
The tools, in one line each
| Tool | What it fundamentally is |
|---|---|
| decknix / deckmacs Agent Shell | An Emacs-native, Nix-configured framework that coordinates multiple AI agents (over ACP) and ties their work to your project's real state — issues, PRs, CI, worktrees. |
| cmux | A native macOS terminal multiplexer (Ghostty) that turns parallel agents and their sub-agents into panes/splits with attention rings. |
| supacode | A native macOS "command center" app (libghostty) for running 50+ CLI agents in parallel, each in its own worktree. |
| Cursor | An AI IDE; its Agents Window launches up to 8 parallel agents, each in an isolated worktree, emitting PRs. |
| OpenAI Codex | A multi-surface agent (CLI + IDE + web + cloud) on one execution model; runs parallel cloud tasks in sandboxes and proposes PRs. |
| Augment Intent | A web workspace for agent orchestration: a coordinator drafts a living spec, implementor agents run in parallel worktrees, a verifier checks. |
| Claude Code | A multi-surface agent (terminal · IDE · desktop · web) with subagents, agent-teams, worktrees and an agent view dashboard — one of the agents decknix drives, not a competitor to it. |
Substrate & parallelism
| Capability | Agent Shell | cmux | supacode | Cursor | Codex | Intent | Claude Code |
|---|---|---|---|---|---|---|---|
| Runs inside your existing editor | ✅ Emacs | ✗ terminal | ✗ app | ◐ its own IDE | ◐ IDE/CLI | ✗ desktop app | ◐ CLI/IDE ext |
| Multi-vendor agents (not one model) | ✅ ACP: Claude Code · Auggie · Pi · Gemini | ✅ any TTY agent | ✅ any CLI agent | ◐ Cursor-managed | ✗ OpenAI only | ✅ BYOA | ✗ Anthropic |
| Run many agents in parallel | ◐ many sessions + sub-agents | ✅ panes | ✅ 50+ | ✅ up to 8 | ✅ cloud queue | ✅ roles | ✅ background agents |
| Sub-agents made visible | ◐ shown (status: roadmap) | ✅ as panes | ✗ independent, not nested | ✅ Agents Window | ✅ collected results | ✅ coord/impl/verify | ✅ agent view / teams |
| Auto worktree-per-agent isolation | ◐ worktree-aware | ◐ scripted | ✅ native | ✅ | ✅ | ✅ | ✅ --worktree |
The right-hand tools win the "fan out eight agents and race them" game — decknix's
model is deliberately one focused session per unit of work, coordinated and
tracked, with parallelism through additional sessions, provider sub-agents, and
the pr-implementer / pr-shepherd workflow. Fan-out-per-worktree is a roadmap
direction (see Sidebar Layouts → WIP), not the current centre
of gravity.
Coordination, provenance & attention
| Capability | Agent Shell | cmux | supacode | Cursor | Codex | Intent | Claude Code |
|---|---|---|---|---|---|---|---|
| Persistent sidebar / overview | ✅ Requests · WIP · Live · Sessions | ✅ tabs | ✅ worktree list | ✅ Agents Window | ✅ app 3-pane | ✅ workspace | ✅ agent view |
| Attention state as colour | ✅ red/amber/green tied to CI · review · age | ✅ blue "needs you" ring | ✅ busy / awaiting / idle badges | ✅ push / iOS | ✅ app + iOS | ◐ verifier | ✅ agent-view state icons |
| PR create / review | ✅ link · review mode · hub reviews | ◐ shows PR # / status | ✅ GitHub-native | ✅ emits PRs | ✅ proposes PRs | ✅ | ✅ commits · PRs · @claude review |
| Cross-service provenance — Jira · Confluence · CI · Slack | ✅✅ hub aggregates Jira · Confluence · GitHub · TeamCity CI (Slack / email / data / support: roadmap) | ✗ git/PR only | ✗ GitHub/CI | ◐ Slack · Linear · GitHub triggers | ◐ Linear · Slack · Jira (MCP) | ◐ Context Engine (MCP) | ◐ Jira · Slack · Linear (MCP) |
| Timeline / history of what happened | ◐ session history · grep · nav (full timeline: roadmap) | ✗ live panes only | ✗ | ✗ | ✗ | ◐ living spec | ✗ |
| Per-conversation resource tracking (PRs authored vs reviewed, tags, worktree, model, mode) | ✅✅ | ✗ | ✗ | ◐ per-agent | ◐ per-task | ◐ per-spec | ◐ per-session |
This is the row that matters most — and note the honest ◐s: several tools now surface attention and can act on Jira/Slack/Linear through MCP connectors. But they reach each service on demand, per connector. Agent Shell instead aggregates and attributes that external state: the hub polls Jira, Confluence, GitHub and CI into one store; the progress layer rolls it up into a red/amber/green attention model; and per-conversation linking attributes PRs (authored vs. reviewed), worktrees and tickets to the session that produced them — answering not just "which agent needs me?" but "what has this session actually done, across every service, and when?"
Openness & extensibility
| Agent Shell | cmux | supacode | Cursor | Codex | Intent | Claude Code | |
|---|---|---|---|---|---|---|---|
| Config-as-code | ✅✅ Nix — declarative, reproducible, org-shareable | ✗ | ✗ | ✗ | ◐ config.toml | ✗ | ◐ CLAUDE.md / settings |
| Extension surface | ✅ Elisp + Nix options + MCP | ✅ socket API / CLI / skills | ◐ CLI / deeplinks | ◐ extensions / MCP | ✅ SDK / MCP / skills / Action | ◐ MCP / Context Engine | ✅ SDK / MCP / hooks / skills |
| Open source | ✅ | ✅ GPL-3.0 | ◐ source-available (FSL) | ✗ | ◐ CLI only (Apache-2.0) | ✗ | ✗ proprietary |
| Cost model | Free / OSS | Free / OSS | Free beta | Paid IDE | Subscription / API | Paid (credits) | Subscription / API |
| Session persistence / resume | ✅✅ resume + fork carry model, permission-mode, tags | ◐ contested | ✅ zmx reattach | ✅ | ✅ resume + fork | ✅ | ✅ resume + fork |
Where it genuinely leads
Pulling the matrix together, four things are rare or absent everywhere else — these are real strengths today, not aspirations:
- It lives in your editor. Every other tool is a terminal, an app, a web workspace, or its own IDE. Agent Shell is a first-class Emacs citizen — agents sit next to your buffers, magit, and org files, driven by the same keybinding muscle memory. (See Keybindings.)
- Config-as-code, org-shareable. Providers, models, MCP servers, sidebar
layout, and review defaults are declared in Nix and rolled out with
decknix switch— reproducible per machine and inheritable across a team's org config. No other tool here ships its coordination setup as version-controlled, composable configuration. (See Integration.) - Vendor-neutral by protocol. ACP lets one interface drive Claude Code, Auggie, Pi, or Gemini — and pin a different model/mode per purpose (e.g. Opus for reviews, a cheaper tier elsewhere). You are not married to one model or one company's roadmap.
- Structured cross-service provenance. Others can act on external services via MCP connectors; the hub + progress layer instead aggregate and attribute them — issues, PRs, CI, reviews (and, on the roadmap, Slack/email, data and support activity) — into a colourised, per-conversation ledger. The competitors stay largely git/PR-centric; decknix models the whole footprint of a piece of work.
Where it falls short — and what to borrow
The honest weak spots, and where a competitor already has the better idea:
- Emacs-native is a floor and a ceiling. It's the point for Emacs users, but real onboarding friction for everyone else — the IDE/terminal tools have a gentler on-ramp. Nothing to borrow here; just a cost to own.
- No one-command fan-out. "Spawn N agents, each in its own worktree, then diff
and pick the winner" is the home turf of Cursor, Codex, Conductor, supacode and
Claude Code's
agent view. decknix is worktree-aware but doesn't yet fan out. Borrow: the worktree-per-agent launch plus a side-by-side diff/pick flow. - Sub-agent status is invisible. Sub-agents are discovered but carry no lifecycle state. Team feedback from trialling cmux specifically praised its per-session "needs input vs still running" indicator and auto-rename by conversation progress. Borrow both: colourise sub-agents by state (the progress layer's red/amber/green already models it) and generate a one-line progress summary (à la Claude Code's agent-view summaries) to auto-name sessions. This is Feature 1 of the resourcing roadmap.
- No in-editor verification surface. cmux's embedded browser for PR review and
rendered docs is genuinely handy. decknix already ships
xwidget-webkitand the/verifyskill — borrow the idea of surfacing them as a first-class review/verify affordance beside the session. - Sub-agents can't talk to each other. Claude Code's experimental agent teams give teammates a mailbox and a shared task list; decknix's sub-agents are report-only. Borrow inter-agent coordination if multi-agent workflows deepen.
- Sessions aren't shareable. Amp's referenceable, team-visible Threads
(
@T-id) are a nice collaboration primitive; decknix sessions are local — worth considering for team review visibility.
Gaps decknix is positioned to bridge
Where the whole field has white space — and decknix already holds the primitives or a head start:
- Multi-human, multi-agent governed pairing. Every tool here is single-human-with-agents. None let two or more people, each with their own agents, share one governed conversation — with full transcripts, durable artifacts, and an explicit path from "we discussed it" to "there is a PR in the right repo." decknix already sketches this direction in its draft Pair Protocol, and the pieces it would build on — the coordination substrate, per-conversation identity, and provenance primitives — are already in place. It is a genuinely open space: nobody in this landscape is building collaboration through agents, only operation of them.
- A cross-service resourcing ledger + timeline. The
C-c s atransient — a tree of a session and its sub-agents, each with what it produced: PRs authored/reviewed, linked issues across Jira and GitHub, worktrees, plus messaging, data, and support activity — over a swimlane timeline of when it happened. The hub, the progress layer, and per-conversation links are the raw material; no competitor aggregates provenance this way. - Accountability, not just fan-out. The field optimises starting many agents; decknix is positioned to own accounting for what they did — the provenance, attention, and audit trail of a piece of work across every service it touched.
The near-term build order (sub-agent state + colourisation → C-c s a resourcing
transient → timeline) is tracked under Sidebar Layouts.
The wider landscape
The matrix samples the field; the fuller taxonomy shows where these tools cluster — and where the Agent Shell sits apart:
- Terminal multiplexers — cmux, supacode: panes + worktrees + attention rings, macOS-native, model-agnostic. Excellent at fan-out; no cross-service provenance. (cmux is GPL-3.0; supacode is FSL source-available, not classic OSS.)
- Desktop orchestrators over agent CLIs — Conductor: free (bring your own subscription), worktree-per-agent, built-in diff/PR flow, start-from-Linear-issue.
- Kanban-as-orchestration — Vibe Kanban: a board that spawns any of ~10 agent CLIs into worktrees; Apache-2.0 — but now community-maintained after Bloop's April 2026 sunset, so weigh its longevity.
- AI IDEs — Cursor: parallel agents + worktrees inside its own editor, plus cloud agents and Slack/Linear/GitHub triggers.
- Multi-surface first-party agents — OpenAI Codex, Sourcegraph Amp: one agent across CLI/IDE/web/cloud; Amp adds native sub-agents and team-shared "Threads." Curated models; Amp has no worktree isolation.
- Enterprise orchestrators — Augment Intent: coordinator → implementors → verifier over bring-your-own agents, worktree-backed.
Across all of them, the Agent Shell is the only one that is simultaneously editor-native (Emacs), declared as Nix config-as-code, and backed by a structured cross-service hub. The others optimise fan-out and PR flow; decknix optimises the coordination, provenance, and accountability of the work — and does it inside the editor you already live in.
Sources: cmux · supacode · Cursor · OpenAI Codex · Augment Intent · Claude Code · Conductor · Vibe Kanban · Sourcegraph Amp. Compiled July 2026; agent tooling moves fast — verify against each tool's current docs.
Sidebar Layouts
The Agents workspace sidebar (C-c A w) stacks four major sections —
Requests → WIP → Live → Sessions — above a Keys/Toggles footer. Each
section has its own row format, glyph set, and (for Requests) a multi-layout
cycle.
These pages mock up the toggle states in colour, because colour is
load-bearing in this UI: it encodes CI state, review state, age, and attention.
A plain-text mock-up (the retired sidebar-demo.txt) could not carry that, so
the layouts now live here.
One page per section:
- Global — org filter, width, footer toggle states
- Requests — the A/B/C/D layout cycle + column anatomy
- WIP — grouping modes today plus a proposed columnar layout
- Live — view modes + linked-PR / linked-repo rows
How to read these mock-ups
Mock-ups use a custom "sidebar DSL" inside <pre class="sb-markup"> blocks.
This allows simple tags like {g}●{/} to render with the real foreground
colours from the Emacs faces.
The colours are faithful to agent-shell/hub/decknix-hub-icons.el and
agent-shell/hub-bulk/decknix-agent-shell-hub.el (cited per page). They are
not screenshots — spacing is approximate; column order and colour are
the contract.
Colour legend (source of truth)
| Swatch | Hex | Semantic | Where it appears |
|---|---|---|---|
| ██ | #98c379 | success / green | approved ●, merged ■, CI pass ⟳, approval ✓, live worktree ⎇*, resolved comments |
| ██ | #e06c75 | error / red | changes-requested ◐/✗, CI fail, conflict ▣/⚠, age ≥3d |
| ██ | #e5c07b | warning / yellow | draft ★, CI running, review-required, bot-pending b, needs-reply, age <3d, ? |
| ██ | #61afef | info / blue | open state word, team @, idle worktree ⎇, branch names |
| ██ | #87d7af | soft green | human reply ↩, bot reply 👽, reply-state comments column |
| ██ | #87d7ff | bright cyan | active-review indicator ◉ |
| ██ | #d7af5f | gold | me @, active-review row tint, needs-reply 💬 |
| ██ | #af5f87 | pink / mauve | bot author π, bot-pending 🤖 |
| ██ | #5c6370 | dim grey | no local clone ↓, stale ⊘ |
| ██ | comment face | dim default | closed ■, sha7, sub-day ages, (none) placeholders |
Shape-family glyphs
The primary status icon folds PR state + CI + review into one glyph
(decknix--hub-primary-status-icon, decknix-hub-icons.el:83):
| Glyph | Meaning | Colour rule |
|---|---|---|
| ○ | placeholder / pre-PR / local branch | shadow |
| ★ | draft | CI: pass / running / fail / orange soft-fail |
| ◐ | open / in-review | blocked / running / cyan commented / else shadow |
| ● | open & approved | success |
| ▣ | merge conflict | error |
| ■ / ■ | merged / closed | merged green, closed dim |
| π | bot author (overrides all above) | pink |
Worktree row badge
A fixed 2-column slot at the start of hub rows
(decknix--hub-worktree-row-badge, hub.el:1672):
| Badge | Meaning |
|---|---|
| ⎇* | branch is checked out in a worktree that is a live session |
| ⎇ | separate worktree of the local clone, no live session yet |
| ↓ | no local clone of the repo on this machine |
·· | (two spaces) primary HEAD / branch ref only / no (repo, branch) context |
Symbol style (y toggle)
y swaps the row-level activity icons (🤖 👽
💬 ↩) between emoji and an
ASCII fallback (β/β/i/i). The Live section count badges (📥 📤) are
always emoji and are not affected by the toggle. The shape-family glyphs and
the columnar ⟳ ✓ ✗ always stay ASCII — Emacs faces cannot tint colour-emoji,
so the colour contract above depends on them being plain glyphs.
Global
Global toggles affect the whole sidebar rather than one section. They live
in the Global group of the T transient and are advertised in the footer.
See the colour legend for the
palette.
| Key | Toggle | States |
|---|---|---|
O | Org filter | [all] ↔ each enabled org (e.g. [upside]) |
W | Width | [narrow] → [med] → [wide] |
K | Keys/Toggles footer | show ↔ hide Navigate/Quick + Toggles lines |
Org filter (O)
Hides every row whose repo is outside the selected org. The section counts
update to match. Default is [all].
{hd}Requests (12){/} ← all orgs
{hd}WIP (4){/}
{hd}Requests (7){/} ← O → upside (reapit/* hidden)
{hd}WIP (2){/}
Width (W)
Width drives truncation and the footer's layout. At narrow/med the footer
toggle groups stack vertically; at wide (≥48 cols) Global+Requests and
Live+WIP render side-by-side so the footer doesn't push content off-screen.
[med] [wide ≥48 cols] Toggles Toggles Global: org [all] w [med] Global: org [all] w [wide] Requests: @ off … Requests: @ off ci [all] … Live: disp [A] view [flat] WIP: linked [hide] … Live: disp [A] view [flat] WIP: linked [hide] stale [on]
Footer toggles & keys (K)
The footer has two parts: the Navigate/Quick key hints and the Toggles
state lines. K hides both so the section content gets the full height; the T
transient still opens and changes them while hidden.
[KEYS SHOWN] [KEYS HIDDEN (K)]
Navigate s sessions r req… {hd}Requests (12){/}
Quick c new k kill …section content only…
Toggles
Global: org [all] w [med]
Requests: @ off ci [all] …
Note: the Toggles state line reflects the current value of every toggle by
label only (no keys) — press T for the interactive transient where the keys
are shown.
Source
- Group definitions:
agent-shell/sidebar/decknix-sidebar-toggles.el - Footer rendering:
agent-shell/sidebar/decknix-sidebar-footer-keys.el
Requests
PR reviews assigned to me, oldest-first by default. This is the only section
with a layout cycle — D in the T transient steps A → B → C → D. All four
render the same four example PRs below so you can compare:
upside#16570— 2d, open, CI pass, approved, I'm @-mentioned, and a live review session is already running (gold tint +◉).upside#16568— 15d, draft, bot-authored, no local clone.reapit#123— 4h, open, CI failing / changes-requested, team requested.upside#16571— 1d, open, CI running, needs my reply.
See the colour legend and shape-family for glyph/colour meanings.
Layout A — Full (default)
wt-badge · age₃ · repo#N · [icon @ activity ◉] · title. The primary icon folds
CI+review into one shape (hub.el:2699).
{hd}Requests (4){/} ⇅
{c}⎇ {/} {y}2d{/} upside{gd}#16570{/} {g}●{/}{gd bd}@{/}{ac}◉{/} {gd}CSE-201: refactor token cache{/}
{dm bd}↓ {/}{r}15d{/} upside#16568 {bo}π{/} {dm}bump: dependency update{/}
{dm bd}↓ {/} {dm}4h{/} reapit#123 {r}◐{/}{c bd}@{/} CSE-204: fix pubsub timeout
{y}1d{/} upside#16571 {y}◐{/}{gd}💬 {/} CSE-205: retry policy for webhook
The row for PR 1 is gold-tinted end to end because a live session is
reviewing it; per-column colours (icon green, @ gold, age yellow) still show
through (add-face-text-property … append, hub.el:2711).
Layout B — Scoped
icon · @activity · title (hub.el:2694). Drops age / repo / number — the
phase-aware minimal signal. ◉ is gone but the active row keeps its gold tint.
{hd}Requests (4){/}
{g}●{/} {gd bd}@{/} {gd}CSE-201: refactor token cache{/}
{bo}π{/} {dm}bump: dependency update{/}
{r}◐{/} {c bd}@{/} CSE-204: fix pubsub timeout
{y}◐{/} {gd}💬 {/} CSE-205: retry policy for webhook
Layout C — Label
icon · state-label₁₆ · title (hub.el:2684). The label is the human-readable
state (dim, fixed 16-col) from decknix--hub-format-row-label.
{hd}Requests (4){/}
{g}●{/} {dm}approved {/} {gd}CSE-201: refactor token cache{/}
{bo}π{/} {dm}draft {/} {dm}bump: dependency update{/}
{r}◐{/} {dm}CI failing {/} CSE-204: fix pubsub timeout
{y}◐{/} {dm}awaiting review{/} CSE-205: retry policy for webhook
Layout D — Minimal
wt-badge · age₄ · detail · icon · repo#N · title (hub.el:2672). High-signal,
compact; the phase is implied by the glyph. This is what the retired
sidebar-demo.txt §5 showed.
{hd}Requests (4){/}
{c}⎇ {/} {y}2d{/} {gd bd}@{/} {g}●{/} upside{gd}#16570{/} {gd}CSE-201: refactor token cache{/}
{dm bd}↓ {/} {r}15d{/} {bo}π{/} upside#16568 {dm}bump: dependency update{/}
{dm bd}↓ {/} {dm}4h{/} {c bd}@{/} {r}◐{/} reapit#123 CSE-204: fix pubsub timeout
{y}1d{/} {gd} 💬 {/} {y}◐{/} upside#16571 CSE-205: retry policy for webhook
Section-header badges
The Requests (N) header grows badges for active filters (hub.el:2581):
{hd}Requests (4){/} {gd bd}@{/} mention filter = me (M-… / @)
{hd}Requests (4){/} {c bd}@{/} mention filter = team
{hd}Requests (4){/} {bo}🤖{/} bot-authors = show
{hd}Requests (4){/} {bo}🤖{/}{gd bd}@{/} bot-authors = mentioned-only
{hd}Requests (4){/} ⇅ sort reversed (newest-first)
Toggles (the T → Requests group)
Each toggle owns one signal — no toggle does another's job, so they compose freely to fine-tune the list. The label beside each key in the transient is spelled out (not icon-only) so the state is unambiguous.
| Key | Toggle | Effect |
|---|---|---|
D | Layout | cycle A → B → C → D |
@ | Mention | off → me → team → me+team |
F | Age | all / 1d / 3d / 7d / 14d / 30d |
C | CI | filter by CI state |
B | bot-authors | hide → show → mentioned |
b | 🤖 bot-pending | hide PRs whose latest activity is a bot (default on) |
c | 💬 needs-my-reply | hide PRs whose latest non-bot activity is someone else, i.e. awaiting my reply (default off) |
o | ⏳ waiting-others | hide PRs where I posted last and am waiting on others (default on) |
M | ↩ replied-to-me | only PRs where a human replied in a thread I took part in |
R | reviewed | cycle show → hide-mine → hide-any (default hide-any) — see below |
s | sort ⇅ | flip oldest↔newest (seeds the r picker) |
X | ⚠ conflict | hide mergeable = CONFLICTING PRs (default on) |
x | 📝 draft | hide draft PRs (default on) |
The reviewed filter (R)
Cycles three states so you can focus on PRs that still need a review from someone:
show— every request, regardless of review status.hide-mine— hide PRs I have already reviewed or commented on (my_review∈ APPROVED / CHANGES_REQUESTED / COMMENTED). PRs only a colleague has touched stay visible.hide-any(default) — also hide PRs a colleague has engaged with: another human's standing review (APPROVED / CHANGES_REQUESTED / COMMENTED) or a conclusive aggregatereview_decision. Someone else is already on it.
In hide-mine and hide-any a PR resurfaces when it genuinely wants
you again:
- a real re-request (you reviewed and the author re-requested you),
- a direct @-mention by name in a comment or review, or
- the review has gone stale — the author moved the PR forward since it was reviewed (a commit landed after it, or changes were requested and every inline thread is now resolved).
These signals (re_requested, comment_mentioned, others_reviewed,
review_stale) are computed by the hub daemon
and read from github-reviews.json; being a merely still-requested
reviewer no longer forces an already-approved PR to stay visible.
Reviewing from the picker (r)
r opens the Requests picker (consult). Select a row to act on it, or —
because the picker no longer requires a match — paste a PR URL that
isn't in the list and press RET to review it directly. The review
launches through the same path as the C-c A c r quick action, so it
inherits the pr-review purpose's provider / model / auto mode.
Source
- Renderer + layout cycle:
agent-shell/hub-bulk/decknix-agent-shell-hub.el:2563 - Primary icon / age / activity icons:
agent-shell/hub/decknix-hub-icons.el - Active-review tint:
decknix--hub-request-tint-active,hub.el:2711
WIP
My own open PRs, grouped by repo. See the colour legend and shape-family.
Two things are orthogonal here:
- Grouping (how rows nest) — shipped today, three modes.
- Row layout (what each row shows) — today a single compact format; proposed below is a layout cycle borrowed from Requests.
Grouping modes (today)
Row format today: wt-badge · icon · age · kind · #N · branch.
Grouping modes — today Row: wt-badge icon age kind #N branch Repo-only (default) WIP (3) decknix ⎇* ● 15d pr #123 feature/foo ↓ π 2d draft #130 dependabot/npm/lodash decknix-config ⎇ ○ 1h wip feature/bar Workspace → repo WIP (4) ~/tools/decknix ⎇* ● 15d pr #123 feature/foo ⎇ ○ 1h wip feature/bar ~/Code/foobar ↓ π 2d draft #130 dependabot/npm/lodash Workspace → repo → worktree WIP (2) decknix ↓ π 2d draft #130 dependabot/npm/lodash foobar monolith ↓ π 2d draft #130 dependabot/npm/lodash
The last row in "Repo-only" is a worktree placeholder — a (repo, branch) in the
worktree registry with no matching open PR yet (⎇ … wip), so freshly-created worktrees
appear before gh pr create indexes (hub.el:2722).
Proposed columnar layout (draft)
Status: proposal for review. Not implemented. Borrows the Requests layout cycle (
D) and the linked-PR signal zone so WIP carries the same scannable pipeline columns as Live's linked-PR rows.
Grouping stays as above; what changes is the row. A new D cycle:
Proposed A — Full (columnar)
wt · #N · age₃ · state₆ · CI · b · c · ✓ · [⚠] · DTSP · branch
(the linked-PR signal zone, reused verbatim).
Proposed columnar layout (draft) Column order: wt #N age state CI b c ✓ [⚠] DTSP branch Proposed A — Full WIP (3) decknix ⎇* #123 2d open ⟳ b c ✓ feature/foo ↓ #130 2d draft ⟳ b · ? ⚠ dependabot/npm/lodash decknix-config ⎇ #— 1h wip feature/bar B — Scoped: wt icon state branch C — Label: wt icon state-label₁₆ branch D — Minimal: icon age kind #N branch (today's default)
Proposed B/C/D
Mirror Requests so muscle memory transfers:
- B — Scoped:
wt · icon · state · branch(phase-aware, drops the signal zone). - C — Label:
wt · icon · state-label₁₆ · branch(e.g.CI failing). - D — Minimal: today's compact row (
icon · age · kind · #N · branch) — keeps the current default as the floor of the cycle.
Open questions for review
- Does the signal zone belong on my own PRs, or is
b/c/✓noise when I'm the author? (Requests uses it for PRs I review.) - Placeholder rows have no PR — should they dim the whole signal zone (shown) or omit it and left-align the branch?
- Should
D(layout) and the grouping toggle share one key or stay separate?
Toggles (the T → WIP group, today)
| Key | Toggle | Effect |
|---|---|---|
L | hide linked | hide PRs already live as sessions |
m | stale | hide MERGED/CLOSED (default on); off shows ⊘ stale rows |
P | pipeline | deploy (DTSP) indicators |
r | ↩ replies-to-me | parallel to Requests, own state |
n | 💬 comments | |
u | 🤖 bot-review |
Source
- WIP renderer:
agent-shell/hub-bulk/decknix-agent-shell-hub.el - Placeholder rows:
decknix--hub-wip-placeholder-rows,hub.el:2722 - Signal-zone formatter (to be reused):
decknix--hub-pr-format-line,hub.el:2003
Live
Live agent sessions. Five view modes (cycle with v / the Live view toggle),
plus optional linked-PR and linked-repo rows expanded beneath each
session. See the colour legend.
Session status family
The leading marker uses the lifecycle shape-family (header-line faces): shape = stage, colour = state.
{dm}○{/} initializing {y}◐{/} working {r}◐{/} waiting (needs input)
{g}●{/} ready {ac}●{/} finished {r}●{/} killed
Provider glyph
Every live-session row is prefixed with a provider glyph — a single letter that identifies the AI backend running in that buffer:
| Glyph | Provider |
|---|---|
A | Auggie (Augment Code) |
C | Claude Code (Anthropic) |
P | Pi |
? | Unknown / unregistered provider |
The glyph is coloured with the same status face as the lifecycle marker so it
reads as part of the same signal group. Additional providers registered via
decknix-agent-register-provider automatically appear here using their :glyph
field.
View modes
A session row is sel · glyph · marker · name · tile · [N⬆ N✓] · 📥/📤/↩/👽 · progress.
> marks the selected buffer. Tags are capped at 3 for readability; full tags
are always visible in the header-line of the buffer itself.
Flat
{hd}Live (3){/}
> {y}A{/} {y}◐{/} feature/foo {rp}decknix{/} [2⬆ 1✓] {r bd}📥1{/}
{g}A{/} {g}●{/} feature/bar {rp}decknix-config{/}
{g}C{/} {g}●{/} review/auth {rp}decknix{/} [1⬆ 0✓] {sg bd}↩{/}
Grouped by workspace
{hd}Live (3){/}
{dm}~/tools/decknix{/}
> {y}A{/} {y}◐{/} feature/foo [2⬆ 1✓] {r bd}📥1{/}
{g}C{/} {g}●{/} review/auth
{dm}~/Code/nurturecloud/decknix-config{/}
{g}A{/} {g}●{/} feature/bar
Grouped by path (last component, tag stripped)
{hd}Live (3){/}
{rp}decknix{/}
> {y}A{/} {y}◐{/} feature/foo [2⬆ 1✓] {r bd}📥1{/}
{g}C{/} {g}●{/} review/auth
{rp}decknix-config{/}
{g}A{/} {g}●{/} feature/bar
Grouped by shared tags
{hd}Live (5){/}
{dm}nurturecloud/CONN{/}
> {y}A{/} {y}◐{/} #10861/ARC
{g}A{/} {g}●{/} #7/#202
{dm}Other{/}
{g}C{/} {g}●{/} review/auth
{r}A{/} {r}◐{/} nurturecloud
{g}A{/} {g}●{/} decknix-config
Grouped by first tag (tree)
{hd}Live (5){/}
{dm}nurturecloud{/}
> {y}A{/} {y}◐{/} CONN/#10861
{g}A{/} {g}●{/} CONN/#7
{dm}review{/}
{g}C{/} {g}●{/} auth
{dm}Other{/}
{g}A{/} {g}●{/} decknix-config
Attention badges (hub enabled): {r bd}📥N{/} linked PRs awaiting
my action, {g bd}📤N{/} ones I've acted on,
{sg bd}↩/👽{/} when any linked PR has replies to me. Terminal
(MERGED/CLOSED) PRs are excluded so stale links don't add noise.
Sub-agent rows
When a session has spawned sub-agents (Claude Code sub-agents are stored in a
subagents/ directory next to the session transcript), they are shown as child
rows beneath the parent, indented by 4 characters and dimmed:
{g}C{/} {g}●{/} review/auth
{dm}↳ C claude-3-5-sonnet{/}
{dm}↳ C computer-use{/}
The provider glyph on sub-agent rows always matches the parent session's backend. The name is the sub-agent's slug (model identifier or tool name).
Linked-PR rows
Shown under a session when the PRs toggle (E) is on. Fixed-width columns so
pipeline progress stays scannable across expand modes
(decknix--hub-pr-format-line, hub.el:2003):
#N · age₃ · state₆ · CI · b · c · ✓ · [⚠] · DTSP
{rp}decknix{/}
#123 {dm}2d{/} {c bd}open {/} {g}⟳{/} {dm}b{/} {sg bd}c{/} {g bd}✓{/} feature/foo
#118 {dm}5d{/} {g}merged{/} {g}⟳{/} {dm}·{/} {dm}·{/} {dm}·{/} hotfix/login
{dm}⊳{/} #99 {dm}1d{/} {y bd}draft {/} {r}⟳{/} {y bd}b{/} {dm}·{/} {y bd}?{/} {r}⚠{/} fork/patch
- state (6-wide, left-pad): {c}open{/} / {y}draft{/} / {g}merged{/} / {dm}closed{/}.
- CI
⟳: {g}pass{/} / {y}running{/} / {r}fail{/} / {dm}idle{/} — always shown. - b bot, c comments, ✓ approval — see the legend. Merged rows
collapse review columns to dim
·; closed rows stop at the state word. - ⚠ appears only on OPEN conflicting rows (
mergeable = CONFLICTING). ⊳prefixes a subject PR (I was added as reviewer).
Linked-repo rows
For repos worked by pushing directly to a branch (linked via C-c A c L).
Intermixed with PR rows under the same repo header
(decknix--hub-repo-format-line, hub.el:2208):
branch · sha7 · age₃ · CI · DTSP
{rp}decknix-config{/}
{br}main{/} {dm}a1b2c3d{/} {dm}3h{/} {g}⟳{/}
{br}staging{/} {dm}9f8e7d6{/} {dm}1d{/} {r}⟳{/}
Repo rows intentionally have no state/bot/comment/approval columns — there's no PR to review.
Toggles (the T → Live group)
| Key | Toggle | Effect |
|---|---|---|
v | view mode | flat → workspace → path → tags → tree → flat |
d | display mode (linked PRs) | off / PR / pipeline / both |
H | hidden | show/hide hidden sessions |
N | repo-name cap | short / medium / full |
E | PRs | off / PR / pipeline / both |
y | symbol style | ascii ↔ emoji |
t | tile | off → 2 → 3 → 4 → off |
Source
- Live renderer:
agent-shell/workspace-bulk/decknix-agent-shell-workspace.el - Linked PR / repo formatters:
agent-shell/hub-bulk/decknix-agent-shell-hub.el
Vision: The Future of AI Tooling in Decknix
The current agent shell is Layer 5 of a broader vision. Here's where it's heading.
Literate Session Export (Next)
Issue: decknix#55
Every AI session becomes publishable knowledge:
C-c E o → Export to Org-mode
C-c E m → Export to Markdown
C-c E h → Export to HTML
C-c E c → Export to Confluence (ADF)
Why this matters: An investigation session becomes a post-mortem. An architecture discussion becomes a design doc. A bug hunt becomes a runbook. The AI conversation is the documentation — export makes it shareable.
Role-Based Workflow Profiles
The agent shell currently serves a single persona: the developer writing code. But AI-assisted workflows extend far beyond coding.
Engineering Workflows
| Workflow | What the Agent Does |
|---|---|
| Investigation | Query logs (GCP MCP), search knowledge base, correlate errors, produce root cause analysis |
| Architecture Review | Analyse codebase structure, identify coupling, suggest decomposition, generate ADRs |
| Incident Response | Real-time log tailing, alert correlation, runbook execution, post-mortem drafting |
| Code Review | Automated review on commit, PR summary generation, review thread resolution |
| Onboarding | Guided codebase exploration, convention explanation, first-task scaffolding |
Transformative Engineering
Beyond individual developer productivity, the tooling enables transformative engineering — systematic, AI-assisted modernisation of large codebases:
| Capability | Description |
|---|---|
| Migration planning | Analyse a legacy codebase, identify migration paths, estimate effort, generate step-by-step plans |
| Pattern extraction | Detect repeated patterns across services, propose shared libraries, generate extraction PRs |
| Observability gap analysis | Compare metric/alert coverage against error hierarchies, identify blind spots |
| Test coverage expansion | Analyse untested paths, generate test scaffolds, prioritise by risk |
| Cross-service coherence | Validate that API contracts, event schemas, and alert definitions stay consistent across services |
Beyond Engineering
The same session-as-first-class-object model applies to non-engineering roles:
| Role | Workflow |
|---|---|
| Product | Spec refinement sessions, user story generation, acceptance criteria drafting |
| QA | Test plan generation, exploratory testing guidance, regression analysis |
| Support | Ticket investigation with knowledge base search, escalation drafting |
| Leadership | Sprint retrospective analysis, technical debt quantification, roadmap impact assessment |
Workflow Templates
Future slash commands and templates will be role-aware:
/investigate <property-id> → Full NC property sync investigation
/incident <alert-name> → Incident response runbook
/review-pr <PR-number> → Structured code review
/onboard <repo-name> → Guided codebase tour
/migrate <from> <to> → Migration planning session
These would combine MCP server access, knowledge base search, and structured output into repeatable workflows.
Multi-Agent Orchestration
The session manager and attention tracker already support multiple concurrent sessions. The next step is coordinated multi-agent workflows:
- Parallel investigation — spawn multiple agents to investigate different aspects of an incident simultaneously
- Review pipeline — one agent reviews code, another checks test coverage, a third validates observability
- Continuous monitoring — background agents that watch CI, alert channels, or deployment status and inject findings into active sessions
Declarative Workflow Definitions
Workflows as Nix configuration:
{ ... }: {
decknix.ai.workflows = {
investigate = {
description = "Full property sync investigation";
mcpServers = [ "gcp-monitoring" "org-knowledge-base" ];
template = "investigate";
context.autoPin = [ "jira" ]; # Auto-pin Jira tickets mentioned
};
incident = {
description = "Incident response runbook";
mcpServers = [ "gcp-monitoring" "pagerduty" ];
template = "incident";
attention.priority = "high"; # Always show in attention tracker
};
};
}
The Endgame
The vision is an environment where:
- Every AI conversation produces artefacts — not just code changes, but documentation, decisions, and knowledge
- Workflows are reproducible — a new team member gets the same investigation tools, templates, and MCP access as a senior engineer
- Context is continuous — switching between sessions preserves the full picture of what you're working on
- The tooling adapts to the role — engineers, product managers, and support staff each get workflows tailored to their needs
- The environment is declarative —
decknix switchreproduces the entire AI-assisted workflow on any machine
decknix CLI
The decknix CLI is a Rust binary that provides the primary interface for managing your configuration.
Usage
decknix [COMMAND]
Commands:
switch Switch system configuration
update Update flake inputs
help Show help (including extensions)
<ext> Run a user-defined extension
The CLI automatically discovers user extensions defined via Nix and displays them alongside built-in commands.
Architecture
┌──────────────┐ ┌──────────────────────────┐
│ Rust Binary │ ──→ │ darwin-rebuild / nix │
│ (decknix) │ │ (actual build commands) │
└──────┬───────┘ └──────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Extension Discovery │
│ /etc/decknix/extensions.json (system) │
│ ~/.config/decknix/extensions.json (home) │
└──────────────────────────────────────────────┘
The binary always runs from ~/.config/decknix/ so --flake .#default resolves correctly.
Getting Help
# Show all commands (built-in + extensions)
decknix help
# Help for a specific command
decknix switch --help
decknix help board
Next
- Core Commands —
switch,update - Extensions — add custom subcommands
Core Commands
decknix switch
Build and activate your configuration.
Usage: decknix switch [OPTIONS]
Options:
--dry-run Build only — don't activate
--force Bypass the preflight equality check and always activate
--override <INPUT=PATH> Override a flake input with a local path (repeatable)
--no-overrides Ignore [switch.overrides] in settings.toml
Examples
# Normal switch (skips sudo activation if nothing changed)
decknix switch
# Dry run (check for errors without activating)
decknix switch --dry-run
# Force re-activation even when the built system matches the current one
decknix switch --force
# Test a local framework checkout
decknix switch --override decknix=~/tools/decknix
# Override multiple inputs (repeat the flag)
decknix switch --override decknix=~/tools/decknix --override nc-config=~/Code/my-org/decknix-config
How It Works
cd ~/.config/decknix- Preflight (unless
--dry-runor--force): evaluates the system derivation vianix build --no-link --print-out-pathsand compares the resulting store path withreadlink /run/current-system.- Match → skips
sudo darwin-rebuild switchentirely, verifies user LaunchAgents (org.nixos.*) are running, kickstarts any that are down, and exits. - Differ → prints old/new store paths and proceeds with activation.
- Match → skips
- Runs
sudo darwin-rebuild switch --flake .#default --impure(reusing the cached preflight build). - For each active override (CLI or
settings.toml), adds--override-input <INPUT> path:<PATH>. - With
--dry-run, usesbuildinstead ofswitchand skips the preflight.
Why the preflight
Once you've applied a configuration, re-running decknix switch with no code
changes should be a fast no-op. The preflight lets Nix's evaluation cache do
the work (typically 1–3s) instead of paying for a full sudo darwin-rebuild switch (30–90s of activation scripts). The --force flag is there for when
you deliberately want to re-run activation — for example, after manually
editing a launchd plist or when debugging an activation script.
Persistent overrides via settings.toml
If you routinely run decknix switch with the same --override flags (e.g.
you keep local checkouts of the framework and your org config), you can pin
them once in ~/.config/decknix/settings.toml:
[switch.overrides]
decknix = "~/tools/decknix"
nc-config = "~/Code/my-org/decknix-config"
Every decknix switch then applies those overrides by default. Precedence,
from highest to lowest:
--override INPUT=PATHon the command line (per-input; wins over config)[switch.overrides]insettings.toml- The published flake inputs (from
flake.lock)
The status line annotates each override with [config] when it came from
settings.toml, so it's always clear where a given path was sourced from:
🔄 Switching (decknix=/Users/you/tools/decknix [config], nc-config=/Users/you/Code/foo/decknix-config [config])...
To force a switch against the published inputs (ignoring settings.toml
entirely), pass --no-overrides:
# Ignore settings.toml — use whatever is pinned in flake.lock
decknix switch --no-overrides
# Ignore settings.toml but apply one one-off override
decknix switch --no-overrides --override decknix=~/experiments/decknix
settings.toml lives alongside your user config; it is a personal file and
should not be checked into a shared decknix-config repo. If your
decknix-config doesn't already ignore it, add it:
settings.toml
decknix update
Update flake inputs (dependencies).
Usage: decknix update [INPUT]
Arguments:
[INPUT] Specific input to update (optional)
Examples
# Update all inputs
decknix update
# Update only decknix
decknix update decknix
# Update only nixpkgs
decknix update nixpkgs
Runs nix flake update [input] under the hood. After updating, run decknix switch to apply.
decknix session
Find, create, resume, and tag agent sessions from the terminal — the workspace- and tag-aware CLI companion to the in-editor session tooling. It resolves against the same session store the Emacs sidebar and pickers use (Claude and Auggie), so a session you spawn in Emacs can be resumed here and vice versa, and tags are shared both ways.
Usage: decknix session <COMMAND>
Commands:
list List sessions in a workspace (default: current directory)
resume Resume a session (exec into the agent by default)
new Start a new session (exec into the agent by default)
tag Add or remove tags on a session's conversation
tags List all known tags with usage counts
decknix session list
List sessions (newest first), scoped to a workspace by default.
Options:
--agent <AGENT> claude, auggie, or all [default: all]
--workspace <WORKSPACE> Workspace to list (default: current directory)
--all List across every workspace instead of just one
--tag <TAGS> Only sessions carrying this tag (repeatable; all must match)
--grep <GREP> Only sessions whose transcript matches this regex
--since <SINCE> Only sessions touched within a window (e.g. 7d, 12h, 30m)
--limit <LIMIT> Cap the number of rows
--json Emit JSON instead of aligned columns
# Sessions in this workspace, all agents
decknix session list
# Claude sessions tagged #dos touched in the last day, as JSON
decknix session list --agent claude --tag dos --since 1d --json
# Everything across every workspace whose transcript matches a regex
decknix session list --all --grep "replay.dlq"
decknix session resume
Resume a session — by id/prefix, by tag (the latest match), or the most recent
in scope. Execs into the agent by default; -n/--print prints the resolved
command instead of running it.
Usage: decknix session resume [OPTIONS] [ID]
Arguments:
[ID] Session id or unique prefix
Options:
--agent <AGENT> claude, auggie, or all [default: all]
--tag <TAGS> Resume the latest session carrying this tag (repeatable)
--last Resume the most recently touched session in scope
--workspace <WORKSPACE> Workspace to resolve within (default: current directory)
--all Resolve across every workspace
-n, --print Print the resolved command instead of exec-ing it
# Resume by id prefix
decknix session resume d8df9eb9
# Resume the latest session tagged #dos in this workspace
decknix session resume --tag dos
# Resume the most recent session anywhere — just print the command
decknix session resume --last --all --print
decknix session new
Start a new session, optionally with an initial prompt (everything after --)
and pre-applied tags.
Usage: decknix session new [OPTIONS] [-- <PROMPT>...]
Arguments:
[PROMPT]... Initial prompt (everything after `--`)
Options:
--agent <AGENT> claude or auggie [default: claude]
--tag <TAGS> Pre-tag the conversation (requires an initial prompt to key it)
--workspace <WORKSPACE> Workspace to start in (default: current directory)
--model <MODEL> Per-conversation model override
-n, --print Print the resolved command instead of exec-ing it
# New Claude session in this workspace
decknix session new
# New session pre-tagged and seeded with a prompt
decknix session new --tag dos --tag triage -- "Investigate ALR-5752"
decknix session tag / decknix session tags
Add or remove tags on a conversation, or list every known tag with usage counts.
# Tag / untag a conversation (by id or unique prefix)
decknix session tag d8df9eb9 --add dos --add day5
decknix session tag d8df9eb9 --remove day5
# List all known tags with counts (--json for machine output)
decknix session tags
decknix session tags --json
Tags are shared with the Emacs session tooling (
C-c A T), so a session tagged here appears under that tag in the sidebar and pickers — and the agents the DoS board spawns on a ticket can be found and resumed later by tag.
decknix wt
Manage git worktrees across all your repos from one place. decknix keeps a
registry of every worktree it knows about (used by the Emacs sidebar too), and
decknix wt inspects and cleans that set — handy when agent work spins up many
short-lived worktrees.
Usage: decknix wt <COMMAND>
Commands:
list List all worktrees from the registry
refresh Re-probe worktrees and update the cache
audit Dry-run report: stale / dirty / orphan-fork / branch-deleted-upstream
orphans List worktrees whose upstream branch has been deleted
clean Clean up old merged worktrees
prune Expunge stale worktrees (directory + branch + metadata + fork-remotes)
prune-metadata Prune git worktree metadata only
clean-fork-remotes Sweep orphan fork remotes
registry Dump the registry
Most cleanup verbs are dry-run by default — they report what they would do
and only act when you add --apply.
Common options (audit / clean / orphans / prune):
-r, --regex <REGEX> Match against repo identifier or worktree path
--older-than <OLDER_THAN> Only worktrees older than a window (e.g. 7d, 12h, 30m)
--apply Actually perform the deletion (else dry-run)
--json Machine-readable output (list / audit / orphans)
# See every worktree decknix tracks (optionally one repo)
decknix wt list
decknix wt list --repo UpsideRealty/pubsub-dlq-forwarder
# Dry-run health report: stale, dirty, orphaned, upstream-branch-deleted
decknix wt audit
decknix wt audit --json
# Worktrees whose upstream branch was deleted (e.g. after a merged PR)
decknix wt orphans
# Remove old, MERGED worktrees — preview, then apply
decknix wt clean --older-than 7d
decknix wt clean --older-than 7d --apply
# Full sweep of stale worktrees (dir + branch + metadata + fork remotes)
decknix wt prune --apply --safe-delete-branch
decknix help
Show help for all commands, including dynamically discovered extensions.
# Show all commands
decknix help
# Help for a specific command or extension
decknix help switch
decknix help board
Extensions show their description and underlying command.
Extensions
The decknix CLI supports user-defined subcommands via a Nix-based extension system.
How It Works
Extensions are defined in Nix and compiled into JSON config files that the Rust binary reads at runtime:
/etc/decknix/extensions.json ← system-level (from programs.decknix-cli.subtasks)
~/.config/decknix/extensions.json ← home-level (from decknix.cli.extensions)
Both files are merged. Extensions appear in decknix help and support --help.
Defining Extensions (Home-Manager)
{ ... }: {
decknix.cli.extensions = {
board = {
description = "Issue dashboard across repos";
command = "${boardScript}/bin/decknix-board";
};
cheatsheet = {
description = "Show WM keybinding cheatsheet";
command = "${cheatsheetScript}/bin/decknix-cheatsheet";
};
};
}
Defining Extensions (System-Level)
# system.nix
{ ... }: {
programs.decknix-cli.subtasks = {
cleanup = {
description = "Garbage collect Nix store";
command = "nix-collect-garbage -d";
pinned = true; # Also creates standalone 'cleanup' command
};
};
}
Setting pinned = true creates a standalone wrapper so you can run cleanup directly without the decknix prefix.
Built-in Extensions
Decknix ships with several extensions:
| Command | Description |
|---|---|
decknix board | Issue dashboard across GitHub repos |
decknix cheatsheet | Show window manager keybinding cheatsheet |
decknix space | Space picker (GUI) |
decknix verify | Verify system integration |
decknix board — cross-repo issue dashboard
Prints a compact, colourised dashboard of GitHub issues across your configured
repos (open/closed counts per repo, then the open issues with their labels). It
is a thin wrapper over gh issue list, so it accepts that command's flags and
passes them through per repo:
# The board (open issues across all configured repos)
decknix board
# Filter by label / assignee / search, or show closed issues
decknix board --label enhancement
decknix board --assignee @me
decknix board --state closed --limit 20
decknix board --search "sidebar in:title"
Requires an authenticated gh (gh auth status). The repo set comes from the
extension's own configuration.
Zsh Completion
Extensions automatically get zsh tab-completion. The module generates a completion script that includes all built-in commands plus discovered extensions.
Using Extensions
# Run an extension
decknix board
# Pass arguments
decknix board open --no-color
# Get help
decknix board --help
# Or:
decknix help board
Arguments after the extension name are passed through as $1, $2, etc.
Decknix Hub
Background work-item aggregator — surfaces PR reviews, WIP PRs, Jira tasks, and CI status in your Emacs sidebar without blocking the editor.
Overview
decknix-hub is a lightweight Rust daemon managed by launchd. It polls
external services (GitHub, Jira, TeamCity) on independent timers and writes
per-adapter JSON files to ~/.config/decknix/hub/. Emacs watches this
directory with file-notify and refreshes the sidebar instantly when data
changes — zero polling from Emacs, zero main-thread blocking.
┌──────────────────────────────────────────────────────┐
│ decknix-hub (launchd) │
│ ┌──────────┐ ┌──────────┐ ┌──────┐ ┌─────────┐ │
│ │ GitHub │ │ GitHub │ │ Jira │ │TeamCity │ │
│ │ Reviews │ │ WIP │ │ 120s │ │ 60s │ │
│ │ 60s poll │ │ 120s │ │ poll │ │ poll │ │
│ └─────┬────┘ └─────┬────┘ └──┬───┘ └────┬────┘ │
│ ▼ ▼ ▼ ▼ │
│ github-reviews github-wip jira-tasks teamcity- │
│ .json .json .json builds.json │
│ └──────────┬──────────────┘ │ │
│ ~/.config/decknix/hub/ │ │
└───────────────────┬───────────────────────────┘
│ file-notify
▼
Emacs sidebar refresh
Quick Start
1. Enable the daemon
In your decknix-config (e.g., ~/.config/decknix/configuration.nix):
decknix.services.hub.enable = true;
2. Apply the configuration
decknix switch
This starts a launchd user agent (com.decknix.hub) that runs in the
background. The Emacs sidebar integration is enabled by default — once the
daemon writes its first data, the Requests and WIP sections appear
automatically.
3. Verify it's running
# Check the launchd agent
launchctl list | grep decknix-hub
# One-shot test (doesn't need launchd)
decknix-hub --once
# Check the data
ls ~/.config/decknix/hub/
cat ~/.config/decknix/hub/meta.json
Configuration Options
Darwin (daemon)
GitHub
| Option | Default | Description |
|---|---|---|
decknix.services.hub.enable | false | Start the launchd daemon |
decknix.services.hub.github.enable | true | Enable GitHub adapter |
decknix.services.hub.github.reviewsInterval | 60 | Seconds between review polls |
decknix.services.hub.github.wipInterval | 120 | Seconds between WIP polls |
decknix.services.hub.github.reviewRepos | [] | Repos to check (empty = all) |
Jira
| Option | Default | Description |
|---|---|---|
decknix.services.hub.jira.enable | false | Enable Jira adapter |
decknix.services.hub.jira.baseUrl | "" | Jira base URL (e.g. https://myorg.atlassian.net) |
decknix.services.hub.jira.email | "" | User email for API auth (typically wired from config.<org>.user.email) |
decknix.services.hub.jira.apiTokenFile | ~/.config/decknix/local/jira-token | Path to Jira API token file |
decknix.services.hub.jira.project | "" | Jira project key (e.g. NC) |
decknix.services.hub.jira.statuses | ["Ready" "In Progress" "Blocked" "Code Review"] | Statuses to poll |
decknix.services.hub.jira.interval | 120 | Seconds between polls |
decknix.services.hub.jira.maxResults | 50 | Max tasks per poll |
TeamCity
| Option | Default | Description |
|---|---|---|
decknix.services.hub.teamcity.enable | false | Enable TeamCity adapter |
decknix.services.hub.teamcity.proxyUrl | http://localhost:8080 | IAP proxy URL |
decknix.services.hub.teamcity.interval | 60 | Seconds between polls |
decknix.services.hub.teamcity.repos | [] | Repos to cross-link with WIP branches |
decknix.services.hub.teamcity.recentFinishedCount | 1 | Recent finished builds per branch |
Identity Wiring
Org configs typically wire identity from config.<org>.user.email (set via identity.nix):
# In org config system.nix:
decknix.services.hub.jira.email = lib.mkDefault config.nurturecloud.user.email;
See Config Loader — Identity Files for details.
Emacs (sidebar integration)
| Option | Default | Description |
|---|---|---|
programs.agent-shell.decknix.hub.enable | true | Show hub data in sidebar |
Sidebar Sections
Requests
PR reviews assigned to you, ordered oldest first. Each line shows:
Requests (8)
72d repo#142 ✓ Fix payment widget
3d repo#219 ⟳ Refactor extract
- Age — colour-coded: ≥3 days = red, <3 days = yellow
- CI —
✓pass,✗fail,⟳running - RET on a line opens the PR in the browser
WIP
Your open PRs grouped by repository, with TeamCity build status:
WIP (3)
my-repo
5h #221 ✓ ✓ feat: new thing
3d #219 ⟳ ⟳42% refactor: extract
other-repo
1d #37 ✗ ✗ fix: update deps
The first icon is GitHub CI, the second is TeamCity (when enabled). TeamCity running builds show progress percentage.
Tasks
Jira tasks assigned to you, grouped by status:
Tasks (4)
● NC-1234 In Progress Implement hub daemon
◐ NC-1235 Code Review Fix payment widget
✕ NC-1236 Blocked Waiting for API access
○ NC-1237 Ready Update documentation
Status icons: ● In Progress, ◐ Code Review, ✕ Blocked, ○ Ready.
Press RET on a task to open it in Jira.
Org Filter (O)
Press O in the sidebar to cycle through GitHub owners/orgs:
all → ldeck → UpsideRealty → all. Filters both Requests and WIP.
Troubleshooting
Sidebar shows "not running — ? O for setup"
The daemon isn't running. Enable it:
decknix.services.hub.enable = true;
Then decknix switch.
Sidebar shows "waiting for data…"
The hub directory exists but no JSON files yet. The daemon may have just started. Check logs:
cat /tmp/decknix-hub.log
gh authentication issues
The daemon uses the gh CLI for GitHub access. Ensure you're authenticated:
gh auth status
Data Files
All state is stored in ~/.config/decknix/hub/:
| File | Content |
|---|---|
github-reviews.json | PR reviews needing your attention (per-PR attention signals — my_review, others_reviewed, re_requested, comment_mentioned, review_stale, review_decision — drive the sidebar's reviewed filter) |
github-wip.json | Your open PRs with CI status and branches |
jira-tasks.json | Jira tasks assigned to you |
teamcity-builds.json | TeamCity build status for WIP branches |
meta.json | Adapter health: last poll time, errors |
Each adapter writes independently — a slow Jira poll won't block GitHub data from refreshing.
Future Adapters (Planned)
- Slack — unread mentions requiring follow-up
- macOS notifications — new reviews, CI failures
Adding Org Configs for Your Team
This guide walks through creating a shared configuration repo for your organisation.
Step 1: Create the Repo
mkdir my-org-config && cd my-org-config
git init
Step 2: Create the Flake
# flake.nix
{
description = "My Org - Decknix Config";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs, ... }: {
darwinModules.default = import ./system.nix;
homeModules.default = import ./home.nix;
};
}
Step 3: Define Team Packages
# home.nix
{ pkgs, ... }: {
home.packages = with pkgs; [
awscli2
terraform
jdk17
nodejs
python3
];
}
# system.nix
{ pkgs, ... }: {
homebrew.casks = [
"docker"
"slack"
];
}
Step 4: Push and Reference
git add . && git commit -m "Initial org config"
git remote add origin git@github.com:MyOrg/decknix-config.git
git push -u origin main
Team members add it to their ~/.config/decknix/flake.nix:
inputs.my-org = {
url = "github:MyOrg/decknix-config";
inputs.nixpkgs.follows = "nixpkgs";
};
And wire the modules:
outputs = inputs@{ decknix, ... }:
decknix.lib.mkSystem {
inherit inputs;
settings = import ./settings.nix;
darwinModules = [ inputs.my-org.darwinModules.default ];
homeModules = [ inputs.my-org.homeModules.default ];
};
Step 5: Test Before Merging
Point decknix switch at your local org-config checkout with
--override <input>=<path> — the input name is whatever you gave it in
flake.nix (e.g. my-org):
decknix switch --override my-org=~/Code/my-org/decknix-config
Or manually, bypassing the CLI:
sudo darwin-rebuild switch --flake .#default --impure \
--override-input my-org path:~/Code/my-org/decknix-config
If your team members will iterate on the org-config regularly, they can pin
the local path once in ~/.config/decknix/settings.toml so plain
decknix switch uses it automatically — see decknix switch → Persistent overrides.
Step 6: Automate Updates
Add Renovate or Dependabot to auto-PR when the org config updates.
Team members pull updates with:
decknix update my-org
decknix switch
Tips
- Keep the org config minimal — only team-wide requirements
- Let individuals override in
~/.config/decknix/<org-name>/ - Add a
bootstrap.shfor one-command onboarding - Include a
secrets.nix.exampleshowing what credentials team members need
Framework Development
This guide covers how to develop and test changes to the decknix framework itself.
Setup
Clone the framework:
git clone git@github.com:ldeck/decknix.git ~/tools/decknix
Testing Changes
Quick Test with --override
The fastest way to test framework changes is to point decknix switch at
your local checkout:
# Edit framework code
$EDITOR ~/tools/decknix/modules/home/options/editors/emacs/default.nix
# Build using local checkout
decknix switch --override decknix=~/tools/decknix
--override INPUT=PATH is repeatable — override the framework and an
org-config at the same time:
decknix switch \
--override decknix=~/tools/decknix \
--override nc-config=~/Code/my-org/decknix-config
Each --override becomes --override-input <INPUT> path:<PATH> on the
underlying darwin-rebuild call.
Persist Overrides for Every Switch
If you always run with the same local checkouts, pin them once in
~/.config/decknix/settings.toml so plain decknix switch picks them up
automatically:
[switch.overrides]
decknix = "~/tools/decknix"
nc-config = "~/Code/my-org/decknix-config"
CLI --override flags win per-input; pass --no-overrides to ignore the
config file for a single run (useful when reproducing an issue against the
published inputs). See decknix switch → Persistent overrides
for the full precedence rules.
Manual Override
If you'd rather bypass the CLI entirely:
cd ~/.config/decknix
sudo darwin-rebuild switch --flake .#default --impure \
--override-input decknix path:~/tools/decknix
Project Structure
decknix/
├── cli/src/main.rs # Rust CLI binary
├── lib/default.nix # mkSystem + configLoader
├── modules/
│ ├── cli/ # CLI nix-darwin module
│ ├── darwin/ # System modules
│ └── home/ # Home-manager modules
│ └── options/
│ ├── cli/ # auggie, board, extensions
│ ├── editors/ # emacs/, vim/
│ └── wm/ # aerospace/, hammerspoon/
├── pkgs/ # Custom packages
└── flake.nix # Framework flake
Adding a New Module
- Create the module file in the appropriate directory:
# modules/home/options/my-tool.nix
{ config, lib, pkgs, ... }:
let cfg = config.decknix.myTool;
in {
options.decknix.myTool.enable = lib.mkEnableOption "My Tool";
config = lib.mkIf cfg.enable {
home.packages = [ pkgs.my-tool ];
};
}
-
The module is auto-imported — all
.nixfiles inmodules/home/options/are loaded. -
Test:
decknix switch --override decknix=~/tools/decknix
Verifying
# Check the generated Emacs config
find /nix/store -name "default.el" -path "*/emacs-packages-deps/*" 2>/dev/null | head -1 | xargs cat
# Check loaded modules
decknix switch --override decknix=~/tools/decknix 2>&1 | grep "\[Loader\]"
# Evaluate an option
nix repl
:lf .
darwinConfigurations.default.config.programs.emacs.decknix.languages.kotlin.enable
Troubleshooting
Emacs Daemon Issues
# Check if daemon is running
launchctl list | grep emacs
# Restart daemon
launchctl stop org.nix-community.home.emacs
launchctl start org.nix-community.home.emacs
# View logs
log show --predicate 'process == "emacs"' --last 1h
Keybindings Not Working
- Did you run
decknix switch? - Check for conflicting configs:
~/.emacs,~/.emacs.d/init.el - Test in Emacs:
M-x describe-key RET <key>
Troubleshooting
Common Issues
"command not found: decknix"
The CLI hasn't been installed yet. Run the full command manually:
sudo darwin-rebuild switch --flake ~/.config/decknix#default --impure
After the first successful switch, decknix will be in your PATH.
Build Errors
Check the loader trace to see which files are being loaded:
decknix switch 2>&1 | grep "\[Loader\]"
Common causes:
- Syntax error in a
.nixfile — check the error message for the file path - Missing input — run
nix flake updateto fetch all inputs - Stale lock file — delete
flake.lockand rebuild
Config Not Taking Effect
- Did you run
decknix switch? - Check that your file is in the right location (the loader traces what it finds)
- Another module may be setting the value with higher priority — try
lib.mkForce
Emacs Daemon Not Starting
# Check service status
launchctl list | grep emacs
# View logs
log show --predicate 'process == "emacs"' --last 1h
# Manually start
launchctl start org.nix-community.home.emacs
Emacs Keybindings Not Working
- Rebuild:
decknix switch - Restart Emacs:
pkill emacs && launchctl start org.nix-community.home.emacs - Check for conflicting configs:
~/.emacs~/.emacs.d/init.el~/.config/emacs/init.el
- Test in Emacs:
M-x describe-key RETthen press the key
Emacsclient Can't Connect
# Check if daemon is running
ps aux | grep "emacs.*daemon"
# Try starting manually
emacs --daemon
# Then connect
emacsclient -c
Debugging
Evaluate Without Building
nix repl
:lf .
darwinConfigurations.default.config.home-manager.users.YOU.home.packages
Check Option Values
nix repl
:lf .
darwinConfigurations.default.config.programs.emacs.decknix.languages.kotlin.enable
Check Generated Emacs Config
find /nix/store -name "default.el" -path "*/emacs-packages-deps/*" 2>/dev/null | head -1 | xargs cat
Reset to Clean State
rm -rf ~/.config/decknix
# Re-run bootstrap or nix flake init -t github:ldeck/decknix
Configuration Hub
Your single entry point for configuring a decknix-managed system. Browse framework options, or jump to upstream references for home-manager, nix-darwin, packages, and casks.
Pair Protocol v1 (draft)
Status: draft / pre-implementation. Reference implementation:
UpsideRealty/experiment-ai-pairing(not yet created at time of writing). Last updated: 2026-05-29.
A protocol for multi-human and multi-agent collaborative sessions layered on top of an agent shell. Designed to be incrementally implementable, mode- switchable, and tolerant of mixed clients (Emacs, web, MCP, CLI, Slack).
This document is the design reference; the implementation is owned by the experiment repository above and may diverge ahead of doc updates while the project is still in its early phases. Where the two disagree, the implementation wins and a doc PR should follow.
1. Goals and non-goals
Goals
- Multi-seat sessions where each seat is a
(participant, role)pair; role ishumanoragent. - Mode-switchable governance — conversational today, driver/multi-driver later — without protocol breaks.
- Two transcript surfaces: a live transcript over WebSocket/SSE, and a long-lived transcript mirrored to a Slack private channel.
- A first-class artifact model — specs, scopes, contracts, architectures — persisted to a durable backend (GCS by default; adapters for Slack, Google Drive, filesystem, git).
- An explicit promote action that takes finished artifacts and opens a
draft pull request against the relevant code repository, with a polite
request for
augmentcodereview baked into the workflow. - Zero-config join for MCP-capable agents; a thin CLI for everyone else.
- Strong audit: every action attributable to a seat, every tool call logged.
Non-goals (v1)
- External (non-staff) participants. Tailnet-only access in P1; Cloudflare Access can be added later if the need arises.
- A fully merged "drive my keyboard" remote-control experience. The protocol reserves driver modes but P1 ships conversational only.
- Replacing existing chat or code review tooling. Slack remains the human mirror; GitHub remains the code review surface.
2. Core concepts
2.1 Sessions and seats
A session is an addressable, time-bounded room with a unique id
(ses_<ULID>). It has:
- a host (the human who created it, holds admin rights in v1);
- zero or more additional seats, each
(participant_id, role)where role ishuman | agent; - a configured mode (see §2.3);
- one or more configured artifact stores (see §5);
- an optional Slack mirror channel (see §4.3);
- an optional repo alias map for
promote(see §6).
Seats are the unit of attribution: every event, message, and tool call
carries the originating seat_id, never just the participant.
2.2 Participants
A participant is the identity behind a seat — a Slack user for humans,
or a named agent registration for agents (e.g. alice/agent, where the
agent runs on Alice's behalf). Identity is resolved against the existing
Slack identity cache so the same human looks the same across Emacs, CLI,
and web clients.
2.3 Modes
| Mode | v1 status | Behaviour |
|---|---|---|
conversational | shipped | All seats may speak; no exclusive driver; agent posts are throttled and addressed-only. |
driver-single | reserved | One driver seat; others are readers and may post questions only. |
driver-handoff | reserved | Driver may pass control to another seat by explicit gesture. |
multi-driver | reserved | Multiple driver seats, admin-curated; readers may ask but not drive. |
Mode is a session-level property; switching modes is an admin-only event. The wire protocol is identical across modes — only the policy gate (§7) differs.
2.4 Transports
The session is exposed over four transports, all of which see the same underlying event stream:
| Transport | Audience |
|---|---|
| WebSocket (live) | Web UI, Emacs, CLI subscribers. |
| Server-Sent Events | Lightweight read-only browser clients. |
| MCP server (per session) | Agents (Claude Desktop, Augment, etc.). |
| Slack mirror | Humans for long-lived browsable history. |
All inbound writes (post a message, publish an artifact, request a promotion) flow through the relay's policy gate before being broadcast.
3. Event envelope
Every event on the wire shares the same JSON envelope:
{
"v": 1,
"id": "evt_01J8A9X0M3T4VWQ0B1C2D3E4F5", // ULID, server-assigned
"ts": "2026-05-29T01:23:45.678Z",
"session_id": "ses_01J7Z8KX4VFWQ0M9N6R2H3D8AB",
"seat_id": "seat_01J7Z8L1...",
"type": "message", // see §3.1
"payload": { /* type-specific */ },
"in_reply_to": "evt_..." // optional, for threading
}
The envelope is stable; payload shape is per type. Unknown types must
be tolerated by clients (forward-compatibility).
3.1 Event types
| Type | Purpose |
|---|---|
session_started | Session created; carries mode, host seat, configured stores. |
seat_joined | New seat enters the session. |
seat_left | Seat exits (voluntary or evicted). |
mode_changed | Admin changed the session mode. |
message | Free-form text from a seat. Markdown-flavoured plain text. |
tool_call | An agent seat ran a tool (name, args, result-hash). |
flag | A seat flagged a moment for human attention (e.g. a question). |
artifact_published | A seat published or superseded a session artifact (§5). |
promotion_requested | A seat asked to open a PR with one or more artifacts (§6). |
promotion_completed | Promotion succeeded or failed; carries the PR URL on success. |
auggie_review_requested | Relay posted the auggie review comment on a promoted PR (§6.4). |
auggie_review_settled | Augmentcode replied with feedback or "nothing to add" (§6.4). |
pr_ready_for_review | A draft PR was flipped to ready for human review (§6.4). |
policy_denied | A request was blocked by the policy gate; carries a reason code. |
audit | Auxiliary audit record (rate-limit hit, admin action, etc.). |
New types may be added; clients ignore unknown types.
3.2 Persistence
The relay writes every event to an append-only JSONL transcript at
~/.local/state/pair-relay/sessions/<session_id>.jsonl (or the equivalent
on a VM). This is the canonical record. The Slack mirror and the
WebSocket fan-out are both derived views. Transcripts are not
committed to VCS; they live on disk and (optionally) in the Slack mirror.
4. Transports
4.1 WebSocket / SSE (live transcript)
- One WebSocket endpoint per session, served by the relay over Tailscale.
- Clients authenticate with a short-lived bearer token minted at invite time (see §8).
- Server pushes events as they happen; clients may post inbound events of
type
message,flag,artifact_published,promotion_requested. - SSE is the read-only fallback for clients that can't open a WebSocket.
4.2 MCP (agent transport)
Each session exposes an MCP server at a session-specific URL. Agents join by pasting the URL plus bearer token into their MCP client config.
Tools exposed (initial set):
| Tool | Effect |
|---|---|
pair.subscribe | Begin receiving the live event stream. |
pair.get_transcript | Fetch the transcript (or a tail) for context. |
pair.post_message | Post a message event (subject to policy). |
pair.ask | Post a flag event explicitly tagged as a question for humans. |
pair.who | List current seats. |
pair.publish_artifact | Upload bytes and emit artifact_published. |
pair.promote_artifact | Request a promotion (§6); subject to admin approval. |
Agents must obey the policy gate (§7); the relay enforces it independent of client co-operation.
4.3 Slack mirror (long-lived transcript)
When a Slack channel is bound at session creation, the relay mirrors a human-readable subset of events into it:
message(rendered with the seat as the author),flag(rendered as a quoted question with a notification ping),artifact_published(file upload with a link to the canonical store),promotion_requested/promotion_completed(PR link as a pinned message on completion),auggie_review_settledandpr_ready_for_review(status updates).
Tool calls and low-level audit events are not mirrored by default; they remain in the JSONL transcript for forensic use.
Humans may type slash commands into the channel:
/pair flag <text>
/pair publish <name> <text>
/pair promote <artifact-ref> to <repo> [as <path>]
The Slack bot parses these, authenticates the user against the bound session, and emits the corresponding event on their seat's behalf.
4.4 CLI (pair)
A thin Rust CLI for humans who prefer the terminal. Same operations as the slash commands plus session lifecycle:
pair create [--mode conversational] [--mirror-channel <id>] [--store <kind>:<config>]
pair join <session-id>
pair say <text>
pair flag <text>
pair publish <file> [--name <name>] [--kind <kind>]
pair promote <artifact-ref>... --to <repo> [--as <path>] [--title <s>] [--draft]
pair list
pair watch <session-id>
The CLI talks to the relay over the same WebSocket endpoint as the web UI.
5. Artifacts and durable storage
5.1 What an artifact is
An artifact is a named, typed blob produced during a session — a scope, a spec, a contract, an architecture sketch, sometimes a transcript snapshot. Artifacts carry:
id— opaque (art_<ULID>);name— human label, unique within a session;kind—scope | spec | contract | architecture | transcript | other;bytes— opaque content;meta— content_type, summary, publishing_seat, optionalsupersedes;ref— backend-opaque locator returned by the store.
Artifacts are explicitly session outputs, not conversation history. They want durable, shareable, queryable storage — not git unless the content is genuinely document-like and worth diffing.
5.2 The ArtifactStore port
The relay holds artifacts through a trait-shaped adapter:
#![allow(unused)] fn main() { trait ArtifactStore { async fn put( &self, session_id: &SessionId, name: &str, kind: ArtifactKind, bytes: &[u8], meta: ArtifactMeta, ) -> Result<ArtifactRef>; async fn get(&self, r: &ArtifactRef) -> Result<Vec<u8>>; async fn list(&self, session_id: &SessionId) -> Result<Vec<ArtifactRef>>; async fn url(&self, r: &ArtifactRef) -> Result<String>; } }
A session may configure one or more stores; the relay writes to all of
them in order on put and reads from the first that resolves on get.
5.3 Built-in adapters
| Adapter | Backing | Fits when… |
|---|---|---|
gcs | Google Cloud Storage bucket | Default for NurtureCloud. Versioned objects, IAM via corporate Google. |
s3 | AWS S3 | Same shape on AWS. |
slack | A bound channel; artifacts as uploads | Lowest-friction for small/transient sessions. |
gdrive | A shared Drive folder | Non-engineer participants want preview + native sharing. |
fs | A local (possibly synced) directory | Laptop relay; quick dev/test. |
git | Any git repo | Opt-in when an artifact will evolve and benefits from named diffs. |
The protocol is store-agnostic; sessions declare what they want:
[[artifact_store]]
kind = "gcs"
bucket = "nc-pair-artifacts"
prefix = "<session_id>/"
# Optional second store, written in parallel for human convenience:
[[artifact_store]]
kind = "slack"
channel_id = "C0XXXXXXXXX"
5.4 Defaults for NurtureCloud sessions
- Primary store:
gcsagainst a bucket likenc-pair-artifacts, object versioning on, lifecycle rule to move >90-day objects to colder storage. - Secondary store (always on by default): the same Slack channel bound for the transcript mirror. The Slack message carries a link back to the canonical GCS object.
- Filesystem fallback is used when the relay runs in laptop mode without GCS credentials (see §10, Phase D1).
The Slack channel becomes the human-discoverable index; GCS is the durable archive.
6. Promotion — opening a PR with finished artifacts
6.1 What promotion is
promote is the explicit gesture that takes one or more session
artifacts and opens a draft pull request against a real code repository.
It is the only protocol-defined integration between the pair tool and a
code repo; everything else stays out of VCS.
Conceptually:
promote(artifact_ids: [Id], target_repo: Repo, target_path: Path,
branch?: String, title?: String, body?: String, draft?: bool = true)
-> PullRequestRef
6.2 Surfaces
Slash command (web UI and Slack mirror):
/pair promote <artifact-ref> to <repo> [as <path>] [titled "..."] [--draft|--ready]
/pair promote @last to upside as docs/specs/pair-protocol.md
/pair promote @kind:spec to upside # bulk by kind
/pair promote scope-pair-protocol-v1 to nct-public-api as docs/scopes/
Artifact refs accepted: art_<ULID>, name, @last, @last:N, @all,
@kind:<kind>.
CLI:
pair promote <ref>... --to <repo> [--as <path>] [--branch <name>] \
[--title "..."] [--body-from <file>] [--draft|--ready]
MCP tool: pair.promote_artifact (same shape as the CLI).
6.3 Per-session repo aliases
Sessions may declare friendly repo aliases at create time so to upside
resolves correctly:
[repos]
upside = "UpsideRealty/upside"
nct-public-api = "UpsideRealty/nct-public-api"
reinz = "UpsideRealty/nct-provider-reinz-service"
experiment = "UpsideRealty/experiment-ai-pairing"
Aliases are resolved against this map; fully-qualified org/repo is
always accepted.
6.4 The draft → augmentcode → ready workflow
Every promotion follows the same polite, reviewer-friendly loop. The tone across all relay-authored comments is soft and collaborative: the relay is asking for help, not issuing orders.
Step 1 — open as draft. Promotions default to draft = true. The
PR title is taken from --title or derived from the artifact name; the
body summarises which artifacts landed and links back to the session.
Step 2 — invite augmentcode. Immediately after the draft PR is open
(and again after every subsequent push to the PR's branch), the relay
posts an auggie review comment. The exact phrasing is fixed so
augmentcode picks it up reliably; surrounding text stays gentle:
Hi @augmentcode — when you have a moment, would you mind taking a look?
auggie review
The relay emits an auggie_review_requested event so the session sees
that review has been asked for.
Step 3 — wait for augmentcode to settle. The relay polls the PR for new review comments from augmentcode until one of two terminal states is reached:
- Comments to consider. Augmentcode left suggestions or questions.
The relay emits
auggie_review_settled { state: "has_feedback" }with a short summary plus a link to the review. The host or driver decides which to apply as fixes and which to politely decline (with a reply explaining the reasoning). After any fix is pushed, Step 2 fires again. - Nothing to add. Augmentcode replied that it has no further
feedback. The relay emits
auggie_review_settled { state: "clean" }and proceeds to Step 4.
Step 4 — flip to ready. Once augmentcode is clean, the relay marks
the PR ready for human review and emits pr_ready_for_review. The
Slack mirror posts the PR link with a brief, polite note such as:
This one is ready for human eyes when you have time — thank you!
The relay does not request specific human reviewers automatically;
that remains a host gesture (or a follow-up Slack post). Human reviewer
assignment via --reviewer @handle on the original promote is a
reserved future option.
Tone rule. All relay-generated PR text — bodies, comments, replies to augmentcode — uses soft, collaborative language: "would you mind", "happy to revise", "thanks for taking a look". No directive or adversarial phrasing.
6.5 Commit and PR shape
Commits opened by the relay carry attribution trailers so the human and the requesting seat are both visible:
Author: Lachlan Deck <lachlan@nurturecloud.com>
Pair-Session: ses_01J7Z8KX4VFWQ0M9N6R2H3D8AB
Pair-Promoted-By: lachlan/human
Pair-Requested-By: alice/agent
Pair-Artifacts: art_01J7..., art_01J7...
Per workspace policy, the relay does not add Co-authored-by:
footers naming itself or augmentcode.
PR body skeleton:
This PR promotes the following artifacts from pair session
[`ses_01J7Z8KX...`](relay-url):
- `scope-pair-protocol-v1.md` → `docs/scopes/pair-protocol-v1.md`
- `spec-pair-protocol-v1.md` → `docs/specs/pair-protocol-v1.md`
It is open as a draft so augmentcode can take a first pass; once that
review settles it will be flipped to ready.
Thanks for taking a look.
6.6 Credentials — phased
| Phase | Mechanism | Notes |
|---|---|---|
| D1 | Relay shells out to the host's local gh CLI. | Laptop mode only; reuses existing host auth; trivially safe. |
| D2 | A GitHub App in the UpsideRealty org with contents:write and pull_requests:write. | Tailnet VM mode. App opens the PR; trailers credit the human + seat. |
| D3 | Same App with finer scopes; per-user OAuth for non-host promoters if needed. | Only if external participants ever land. |
Start at D1. Migrate to D2 when the relay moves off the laptop.
6.7 Direct push for experiment- repos
For repositories whose name starts with experiment- (e.g.
UpsideRealty/experiment-ai-pairing), the relay accepts an additional
--direct flag on promote that commits straight to the repo's default
branch without opening a PR. This is reserved for the implementation
repo itself while it iterates rapidly. If the project graduates out of
experiment- status (rename) the flag stops being honoured.
--direct and --draft are mutually exclusive; --direct skips the
augmentcode loop entirely. The commit trailers and tone rules of §6.5
still apply.
7. Governance and policy gate
Every inbound write passes through the relay's policy gate before being broadcast. The gate is the only enforcement boundary; clients are not trusted.
7.1 Decisions
Per (action, mode, seat-role) the gate decides one of:
ALLOW— broadcast and persist.DEFER → host— emit aflagto the host; broadcast only after the host'spair admitevent.DENY— emit apolicy_deniedevent with a reason code; do not broadcast.
7.2 Matrix (v1)
| Action / Mode | conversational | driver-single | driver-handoff | multi-driver |
|---|---|---|---|---|
message (human, non-driver) | ALLOW | ALLOW | ALLOW | ALLOW |
message (agent, addressed-to-it) | ALLOW | ALLOW | ALLOW | ALLOW |
message (agent, unaddressed) | RATE-LIMITED | DENY | DENY | RATE-LIMITED |
flag (any seat) | ALLOW | ALLOW | ALLOW | ALLOW |
artifact_published (any seat) | ALLOW | DRIVER ONLY | DRIVER ONLY | DRIVER ONLY |
promotion_requested (host-human) | ALLOW | ALLOW | ALLOW | ALLOW |
promotion_requested (driver, any role) | n/a | DEFER → host | DEFER → host | DEFER → host |
promotion_requested (other seat / agent) | DEFER → host | DEFER → host | DEFER → host | DEFER → host |
mode_changed (any seat) | HOST ONLY | HOST ONLY | HOST ONLY | HOST ONLY |
evict / admit | HOST ONLY | HOST ONLY | HOST ONLY | HOST ONLY |
Admin actions (mode_changed, evict, admit, and approval of any
DEFER → host decision) are restricted to the host-human seat in
v1. A future toggle may relax this to designated admin seats.
7.3 Defaults that protect against agent noise
- Agents default to "silent + addressed-only": an agent only posts
when a
messageis addressed to its handle, or when its host has asked it to opine viapair.post_messagewith an explicit gesture. - Rate limits per agent seat: at most one
messageper N seconds (configurable; default 5s), with bursts up to 3 events. - Loop guard (§9) prevents agents from auto-responding to other agents' messages.
8. Identity, auth, and network perimeter
8.1 Identity
Humans authenticate against Slack OIDC; their Slack user id is the
canonical participant id. Agent participants register with a
relay-issued handle of the form <human>/agent (e.g. alice/agent) so
each agent is tied to a responsible human.
8.2 Tokens
Joining a session uses a short-lived bearer token (default TTL 24h)
minted by the relay at invite time. Tokens are scoped to a single
(session_id, seat_id) pair. The relay rotates the signing key on a
fixed cadence.
8.3 Network perimeter
| Phase | Exposure | Notes |
|---|---|---|
| D1 | localhost only (laptop launchd) | Host uses the relay directly; no remote participants. |
| D2 | Tailscale (tailnet members only) | Default for in-team pairing. ACLs restrict to staff. |
| D3 | Cloudflare Access in front of D2 | Only if external participants ever need to join. |
P1 ships D1 with a clear migration path to D2.
9. Loop guard and rate limiting
Two relay-side mechanisms prevent agent storms:
- Loop guard. An agent seat may not respond to a
messagewhose originating seat is also an agent unless that message explicitly@-mentions the responding agent's handle. The gate enforces this independent of client co-operation. - Token-bucket rate limiting. Each agent seat has a bucket
(default: 3 token capacity, refill 1 token every 5s). Exhausted
buckets cause
messageevents to be dropped with apolicy_deniedevent of reasonrate_limited.
Both can be tuned per-session by the host; sane defaults ship in P1.
10. Phasing and roadmap
10.1 Protocol phasing (P*)
| Phase | Scope | Target |
|---|---|---|
| P0 | This spec, repo bootstrap (UpsideRealty/experiment-ai-pairing), CI skeleton. | This week. |
| P1 | Relay daemon (Rust), conversational mode only, JSONL transcript, WebSocket transport, single-host launchd, basic web UI. | This week / next. |
| P2 | Slack mirror, GcsArtifactStore + FilesystemArtifactStore, publish_artifact, MCP transport. | Next. |
| P3 | pair CLI parity with web UI; slack artifact adapter; identity polish. | Following. |
| P3.5 | promote command end-to-end, including the augmentcode review loop (§6.4). Starts on D1 credentials. | Following. |
| P4 | Driver modes (driver-single, driver-handoff), eviction, admin polish. | When wanted. |
| P5 | multi-driver, gdrive and git artifact adapters, optional Cloudflare exposure (D3). | Later. |
P1 is the smallest thing that lets two people pair through the relay in conversational mode with a JSONL transcript on disk. Everything else plugs in behind the stable interfaces above.
10.2 Deployment phasing (D*)
D1 (laptop launchd) → D2 (tailnet VM) → D3 (Cloudflare Access on top). These are independent of the protocol phases; D1 is the only one P1 requires.
11. Defaults summary
| Concern | Default |
|---|---|
| Mode | conversational |
| Admin | Host-human seat only |
| Live transport | WebSocket (SSE fallback) |
| Agent transport | MCP |
| Slack mirror | Enabled when a channel is bound; carries human-readable subset |
| Primary artifact store | gcs (nc-pair-artifacts bucket, versioning on) |
| Secondary artifact store | slack (bound channel, file upload with link to canonical store) |
Promotion draft | true |
| Augmentcode review | Always requested on every push to a promoted PR (auggie review comment) |
| Ready-flip | Manual gesture if augmentcode has feedback; automatic when augmentcode is clean |
| Network perimeter | D1 (localhost) for P1; D2 (Tailscale) when off-laptop |
| Agent rate limit | 3 burst, 1 token / 5s refill |
| Loop guard | On |
| Event ids | ULID |
| Implementation language | Rust (relay + CLI) |
| Implementation repo | UpsideRealty/experiment-ai-pairing (direct-push allowed while named experiment-) |
12. Open questions
These remain for the next round (none of them block P1):
- Promoted-PR human-reviewer assignment. Should
pair promote --reviewer @handle(or a per-session default list) become part of P3.5, or do we keep that as a manual step after the relay flips the PR to ready? - Multi-store consistency. When
gcsandslackare both configured and the Slack write fails after the GCS write succeeds, should the relay retry, log-and-continue, or surface a visible warning to the session? Default proposal: log-and-continue with anauditevent. - Augmentcode "settled" detection. What's the most robust signal for "augmentcode has nothing further to add"? Options include a sentinel comment from the bot, a review with no comments and no change requests, or an explicit phrase match. Implementation will pick the most reliable available at the time and document it.
- Driver hand-off gesture. P4 territory — slash command or button in the web UI? Both eventually, but which lands first?
13. Glossary
| Term | Meaning |
|---|---|
| Session | An addressable, time-bounded pair room with a stable id. |
| Seat | A (participant, role) tuple; the unit of attribution. |
| Participant | The identity behind one or more seats (a Slack user, or an agent registration). |
| Mode | The governance state of a session (conversational, driver-*, multi-driver). |
| Host | The human who created the session; in v1 also the sole admin. |
| Driver | A seat with write-control authority in driver modes (not used in v1). |
| Reader | A seat without write-control authority in driver modes (not used in v1). |
| Artifact | A named, typed blob produced during a session. |
| Artifact store | Backend that durably persists artifacts; one of gcs/s3/slack/gdrive/fs/git. |
| Promote | Explicit action that opens a draft PR in a code repo from one or more artifacts. |
| Auggie review | The auggie review comment the relay posts to invite augmentcode feedback. |
| Policy gate | Relay-side enforcement layer that decides ALLOW / DEFER / DENY on every inbound write. |
| Loop guard | Prevents agents from auto-replying to other agents without an @-mention. |
| D1 / D2 / D3 | Deployment phases (laptop / tailnet VM / Cloudflare Access). |
| P0 / P1 / … | Protocol implementation phases. |
External Resources
Nix
- Nix Manual — official Nix reference
- nix.dev — community tutorials and guides
- Nix Flakes — flakes overview on NixOS Wiki
- Nixpkgs Search — find packages by name
nix-darwin
- nix-darwin Manual — macOS system options
- nix-darwin GitHub — source and issues
Home Manager
- Home Manager Manual — all home-manager options
- Home Manager Options Search — searchable options index
- Home Manager GitHub — source and issues
Emacs
- GNU Emacs Manual
- Magit Manual — Git interface
- Forge Manual — GitHub/GitLab integration
- Vertico — completion UI
- Consult — enhanced commands
- Eglot Manual — built-in LSP client
Window Management
- AeroSpace — tiling WM for macOS
- Hammerspoon — macOS automation
AI Tooling
- Augment Code — AI coding agent
- Model Context Protocol — MCP specification
Decknix
- Framework Repo — source, issues, releases