Compare commits

..

3 commits

Author SHA1 Message Date
5847c04acd feat: add the files for claude code configuration 2026-02-06 22:50:40 +01:00
248534bc35 feat(dev): deploy claude-code config via systemd instead of home-manager
Home-manager fails on the dev microVM because nix-env --set needs
writable nix state dirs, but the microVM shares /nix/store read-only
via virtiofs.

Extract shared claude-code settings into settings.nix, add a NixOS
module (nixos.nix) that deploys the same files via a systemd oneshot
service with RequiresMountsFor to handle virtiofs mount ordering.
The nb host continues using home-manager unchanged.
2026-02-06 22:49:55 +01:00
4648d6b51a add placeholder for matrix implementation 2026-02-06 21:33:57 +01:00
15 changed files with 670 additions and 80 deletions

View file

@ -29,6 +29,7 @@ in
{
imports = [
./modules/dev-tools.nix
./users
];
networking.hostName = "dev";

View file

@ -0,0 +1,3 @@
{
imports = [ ./dominik.nix ];
}

View file

@ -0,0 +1,6 @@
{ config, lib, pkgs, ... }:
{
imports = [
../utils/home-manager/claude-code/nixos.nix
];
}

View file

@ -10,22 +10,104 @@ let
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON data}';
'';
# Shared settings format for bridges
settingsFormat = pkgs.formats.json {};
in {
sops.secrets.matrix-shared-secret = {
};
sops.secrets.dendrite-private-key = {
# Secrets for Synapse
sops.secrets.synapse-oidc-client-secret = {
owner = "matrix-synapse";
};
# PostgreSQL database for Synapse
services.postgresql = {
enable = true;
ensureDatabases = [ "dendrite" ];
ensureDatabases = [ "matrix-synapse" ];
ensureUsers = [
{
name = "dendrite";
name = "matrix-synapse";
ensureDBOwnership = true;
}
];
};
services.postgresqlBackup.enable = true;
services.postgresqlBackup.databases = [ "matrix-synapse" ];
# Synapse homeserver
services.matrix-synapse = {
enable = true;
settings = {
server_name = "cloonar.com";
public_baseurl = baseUrl;
listeners = [
{
port = 8008;
bind_addresses = [ "::1" ];
type = "http";
tls = false;
x_forwarded = true;
resources = [
{
compress = true;
names = [ "client" "federation" ];
}
];
}
];
database = {
name = "psycopg2";
args = {
host = "/run/postgresql";
database = "matrix-synapse";
user = "matrix-synapse";
};
};
# Disable registration - users created via OIDC
enable_registration = false;
allow_guest_access = false;
# OIDC SSO via Authelia
oidc_providers = [
{
idp_id = "authelia";
idp_name = "Authelia";
discover = true;
issuer = "https://auth.cloonar.com";
user_profile_method = "userinfo_endpoint";
client_id = "synapse";
client_secret_path = config.sops.secrets.synapse-oidc-client-secret.path;
scopes = [ "openid" "profile" "email" ];
allow_existing_users = true;
user_mapping_provider.config = {
subject_claim = "sub";
localpart_template = "{{ user.preferred_username }}";
display_name_template = "{{ user.name }}";
email_template = "{{ user.email }}";
};
}
];
# Appservice registrations for bridges
app_service_config_files = [
"/var/lib/mautrix-whatsapp/whatsapp-registration.yaml"
"/var/lib/mautrix-signal/signal-registration.yaml"
"/var/lib/mautrix-discord/discord-registration.yaml"
];
};
};
# Allow bridge users to read registration files
systemd.services.matrix-synapse.serviceConfig.SupplementaryGroups = [
"mautrix-whatsapp"
"mautrix-signal"
"mautrix-discord"
];
# Element Web client
services.nginx.virtualHosts."element.cloonar.com" = {
forceSSL = true;
enableACME = true;
@ -46,9 +128,7 @@ in {
};
};
services.postgresqlBackup.enable = true;
services.postgresqlBackup.databases = [ "dendrite" ];
# Synapse nginx reverse proxy
services.nginx.virtualHosts."${fqdn}" = {
forceSSL = true;
enableACME = true;
@ -56,60 +136,28 @@ in {
locations."/".extraConfig = ''
return 404;
'';
locations."/_dendrite".proxyPass = "http://[::1]:8008";
locations."/_matrix".proxyPass = "http://[::1]:8008";
locations."/_synapse/client".proxyPass = "http://[::1]:8008";
};
#
# Mautrix bridges
#
services.dendrite = {
enable = true;
settings = {
global = {
server_name = "cloonar.com";
private_key = "$CREDENTIALS_DIRECTORY/private_key";
database.connection_string = "postgresql:///dendrite?host=/run/postgresql";
};
client_api.registration_shared_secret = "$REGISTRATION_SHARED_SECRET";
app_service_api.config_files = [
"$CREDENTIALS_DIRECTORY/whatsapp_registration"
"$CREDENTIALS_DIRECTORY/signal_registration"
"$CREDENTIALS_DIRECTORY/discord_registration"
];
app_service_api.database.connection_string = "";
federation_api.database.connection_string = "";
key_server.database.connection_string = "";
relay_api.database.connection_string = "";
media_api.database.connection_string = "";
room_server.database.connection_string = "";
sync_api.database.connection_string = "";
user_api.account_database.connection_string = "";
user_api.device_database.connection_string = "";
mscs.database.connection_string = "";
};
loadCredential = [
"private_key:${config.sops.secrets.dendrite-private-key.path}"
"whatsapp_registration:/var/lib/mautrix-whatsapp/whatsapp-registration.yaml"
"signal_registration:/var/lib/mautrix-signal/signal-registration.yaml"
"discord_registration:/var/lib/mautrix-discord/discord-registration.yaml"
];
environmentFile = config.sops.secrets.matrix-shared-secret.path;
};
# WhatsApp bridge
users.users.mautrix-whatsapp = {
isSystemUser = true;
group = "mautrix-whatsapp";
home = "/var/lib/mautrix-whatsapp";
description = "Mautrix-WhatsApp bridge user";
};
users.groups.mautrix-whatsapp = {};
systemd.services.mautrix-whatsapp = let
dataDir = "/var/lib/mautrix-whatsapp";
registrationFile = "${dataDir}/whatsapp-registration.yaml";
settingsFile = "${dataDir}/config.json";
settingsFileUnsubstituted = settingsFormat.generate "mautrix-whatsapp-config-unsubstituted.json" defaultConfig;
settingsFormat = pkgs.formats.json {};
appservicePort = 29318;
defaultConfig = {
homeserver = {
@ -154,10 +202,9 @@ in {
};
in {
description = "Mautrix-WhatsApp Service - A WhatsApp bridge for Matrix";
wantedBy = ["multi-user.target"];
wants = ["network-online.target"];
after = ["network-online.target"];
wants = ["network-online.target" "matrix-synapse.service"];
after = ["network-online.target" "matrix-synapse.service"];
preStart = ''
test -f '${settingsFile}' && rm -f '${settingsFile}'
@ -189,7 +236,6 @@ in {
serviceConfig = {
User = "mautrix-whatsapp";
Group = "mautrix-whatsapp";
# EnvironmentFile = cfg.environmentFile;
StateDirectory = baseNameOf dataDir;
WorkingDirectory = dataDir;
ExecStart = ''
@ -225,19 +271,19 @@ in {
restartTriggers = [settingsFileUnsubstituted];
};
# Signal bridge
users.users.mautrix-signal = {
isSystemUser = true;
group = "mautrix-signal";
home = "/var/lib/mautrix-signal";
description = "Mautrix-Signal bridge user";
};
users.groups.mautrix-signal = {};
systemd.services.mautrix-signal = let
pkgswithsignal = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/fd698a4ab779fb7fb95425f1b56974ba9c2fa16c.tar.gz") {
config = {
permittedInsecurePackages = [
# needed for matrix
"olm-3.2.16"
];
};
@ -246,7 +292,6 @@ in {
registrationFile = "${dataDir}/signal-registration.yaml";
settingsFile = "${dataDir}/config.json";
settingsFileUnsubstituted = settingsFormat.generate "mautrix-signal-config-unsubstituted.json" defaultConfig;
settingsFormat = pkgs.formats.json {};
appservicePort = 29328;
defaultConfig = {
homeserver = {
@ -295,10 +340,9 @@ in {
};
in {
description = "Mautrix-Signal Service - A Signal bridge for Matrix";
wantedBy = ["multi-user.target"];
wants = ["network-online.target"];
after = ["network-online.target"];
wants = ["network-online.target" "matrix-synapse.service"];
after = ["network-online.target" "matrix-synapse.service"];
preStart = ''
test -f '${settingsFile}' && rm -f '${settingsFile}'
@ -331,7 +375,6 @@ in {
serviceConfig = {
User = "mautrix-signal";
Group = "mautrix-signal";
# EnvironmentFile = cfg.environmentFile;
StateDirectory = baseNameOf dataDir;
WorkingDirectory = dataDir;
ExecStart = ''
@ -367,20 +410,19 @@ in {
restartTriggers = [settingsFileUnsubstituted];
};
# Discord bridge
users.users.mautrix-discord = {
isSystemUser = true;
group = "mautrix-discord";
home = "/var/lib/mautrix-discord";
description = "Mautrix-Discord bridge user";
};
users.groups.mautrix-discord = {};
systemd.services.mautrix-discord = let
pkgswithdiscord = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/5ed627539ac84809c78b2dd6d26a5cebeb5ae269.tar.gz") {
config = {
permittedInsecurePackages = [
# needed for matrix
"olm-3.2.16"
];
};
@ -389,7 +431,6 @@ in {
registrationFile = "${dataDir}/discord-registration.yaml";
settingsFile = "${dataDir}/config.json";
settingsFileUnsubstituted = settingsFormat.generate "mautrix-discord-config-unsubstituted.json" defaultConfig;
settingsFormat = pkgs.formats.json {};
appservicePort = 29329;
defaultConfig = {
homeserver = {
@ -436,10 +477,9 @@ in {
};
in {
description = "Mautrix-Discord Service - A Discord bridge for Matrix";
wantedBy = ["multi-user.target"];
wants = ["network-online.target"];
after = ["network-online.target"];
wants = ["network-online.target" "matrix-synapse.service"];
after = ["network-online.target" "matrix-synapse.service"];
preStart = ''
test -f '${settingsFile}' && rm -f '${settingsFile}'
@ -472,7 +512,6 @@ in {
serviceConfig = {
User = "mautrix-discord";
Group = "mautrix-discord";
# EnvironmentFile = cfg.environmentFile;
StateDirectory = baseNameOf dataDir;
WorkingDirectory = dataDir;
ExecStart = ''
@ -506,4 +545,5 @@ in {
};
restartTriggers = [settingsFileUnsubstituted];
};
}

View file

@ -1,5 +1,20 @@
{ config, pkgs, lib, ... }:
let
sddmTheme = pkgs.where-is-my-sddm-theme.override {
themeConfig.General = {
showUsersByDefault = true;
background = "/nix/persist/system/wallpaper.png";
backgroundFill = "#252525";
backgroundFillMode = "Image.Pad";
passwordInputWidth = 0.25;
passwordInputBackground = "#60ffffff";
passwordFontSize = 28;
showSessionsByDefault = true;
sessionsFontSize = 24;
usersFontSize = 32;
};
variants = ["qt5"];
};
in {
imports = [
../sway/sway.nix
@ -23,21 +38,7 @@ in {
openscad
orca-slicer
(where-is-my-sddm-theme.override {
themeConfig.General = {
showUsersByDefault = true;
background = "/nix/persist/system/wallpaper.png";
backgroundFill = "#252525";
backgroundFillMode="Image.Pad";
passwordInputWidth = 0.25;
passwordInputBackground = "#60ffffff";
passwordFontSize = 28;
showSessionsByDefault = true;
sessionsFontSize=24;
usersFontSize=32;
};
variants = ["qt5"];
})
sddmTheme
dracula-theme
foot
@ -86,6 +87,7 @@ in {
enable = true;
wayland.enable = true;
theme = "where_is_my_sddm_theme_qt5";
extraPackages = [ sddmTheme ];
};
xdg.portal = {

View file

@ -188,7 +188,9 @@ in
};
home-manager.users.dominik = { lib, pkgs, ... }: {
# imports = [ "${impermanence}/home-manager.nix" ];
imports = [
../utils/home-manager/claude-code
];
/* The home.stateVersion option does not have a default and must be set */
home.stateVersion = "25.05";
home.enableNixpkgsReleaseCheck = false;

View file

@ -254,6 +254,21 @@ in {
];
userinfo_signing_algorithm = "none";
}
# {
# id = "synapse";
# description = "Matrix Synapse homeserver";
# secret = "$pbkdf2-sha512$310000$PLACEHOLDER_NEEDS_UPDATING$PLACEHOLDER_NEEDS_UPDATING";
# public = false;
# authorization_policy = "one_factor";
# redirect_uris = [ "https://matrix.cloonar.com/_synapse/client/oidc/callback" ];
# consent_mode = "implicit";
# scopes = [
# "openid"
# "profile"
# "email"
# ];
# userinfo_signing_algorithm = "none";
# }
];
};
};

View file

@ -0,0 +1,61 @@
---
name: devil-advocate
description: Devil's advocate code reviewer that critically examines recent changes for bugs, edge cases, security issues, and architectural violations. Use when you want a thorough adversarial review of code changes, or it runs automatically as a Stop hook.
tools: Read, Bash, Grep, Glob
model: opus
---
You are a devil's advocate code reviewer. Your job is to find problems that the developer missed. Be thorough, skeptical, and constructive.
## Review Process
1. **Get the diff**: Run `git diff HEAD` to see unstaged changes, and `git diff --cached` to see staged changes. If both are empty, run `git diff HEAD~1` to review the last commit.
2. **Read project-specific conventions**: Read `.claude/devil-advocate.md` from the current working directory (if it exists). This file contains project-specific rules and conventions you MUST enforce. If the file doesn't exist, proceed with a generic review.
3. **Read changed files in full**: For each file in the diff, read the complete file to understand context around the changes.
4. **Search for related code**: Use Grep and Glob to find callers, tests, related types, and other code that might be affected by the changes.
5. **Perform the review** checking these categories:
- **Bugs & Logic Errors**: Off-by-one, null/undefined access, race conditions, incorrect conditions, missing return values
- **Edge Cases**: Empty inputs, boundary values, concurrent access, error paths not handled
- **Error Handling**: Swallowed errors, missing try/catch, unhelpful error messages, unhandled promise rejections
- **Security**: Injection vulnerabilities, exposed secrets, missing input validation, insecure defaults
- **Architecture Violations**: Breaking project conventions from `.claude/devil-advocate.md`, wrong layer for the operation, circular dependencies
- **Data Integrity**: Missing transactions, partial updates that could corrupt state, sync issues
- **Breaking Changes**: API contract changes, removed fields still referenced elsewhere, changed behavior without updating callers
## Output Format
If you find issues, respond with a structured report:
```
ISSUES FOUND:
[CRITICAL] file.dart:42 - Description of the bug
→ Suggested fix: ...
[HIGH] file.dart:88 - Description of the issue
→ Suggested fix: ...
[MEDIUM] file.dart:15 - Description of the concern
→ Suggested fix: ...
```
Severity levels:
- **CRITICAL**: Will cause crashes, data loss, or security vulnerabilities
- **HIGH**: Likely to cause bugs in production or violates critical project conventions
- **MEDIUM**: Code smell, minor convention violation, or potential future issue
Only report issues you are confident about. Do NOT report:
- Style preferences or nitpicks
- Missing documentation or comments
- Hypothetical issues that require unlikely conditions
- Things that are clearly intentional based on context
## Decision
- If you find CRITICAL or HIGH issues: these MUST be fixed before the session ends.
- If you only find MEDIUM issues or no issues: the code is acceptable.
- If there are no meaningful changes to review (empty diff): the code is acceptable.

View file

@ -0,0 +1,114 @@
---
name: lint-fixer
description: Auto-detects and runs the project's linter/formatter. Fixes auto-fixable issues and reports blocking errors. Passes through when no linter is detected.
tools: Bash, Read, Edit, Write, Grep, Glob
model: sonnet
---
You are a lint/format fixer agent. Your job is to detect the project's linter and formatter, run them, auto-fix what you can, and report any remaining issues.
**You have full access to Bash, Edit, and Write tools. Always use Bash to run fix commands first (e.g., `dart fix --apply`). Use Edit/Write for any remaining issues that CLI tools can't auto-fix. Do not hesitate to execute commands and edit files — that is your primary purpose.**
## Process
### 1. Check for project-specific config
Read `.claude/lint-fixer.md` from the current working directory. If it exists, it contains explicit lint/format commands to run and any overrides. Follow those instructions exactly.
### 2. Auto-detect linter/formatter
If no project-specific config exists, detect the linter/formatter by checking for these files (in order):
| File / Pattern | Tool | Fix Command |
|---|---|---|
| `pubspec.yaml` (Flutter/Dart) | dart analyze | `dart fix --apply` then `dart format .` |
| `package.json` + `.eslintrc*` or `eslint.config.*` | ESLint | `npx eslint --fix .` |
| `package.json` + `.prettierrc*` or `prettier` in devDeps | Prettier | `npx prettier --write .` |
| `pyproject.toml` with `[tool.ruff]` | Ruff | `ruff check --fix . && ruff format .` |
| `pyproject.toml` with `[tool.black]` | Black | `black .` |
| `setup.cfg` or `tox.ini` with `[flake8]` | Flake8 | `flake8 .` (no auto-fix) |
| `.golangci.yml` or `go.mod` | golangci-lint | `golangci-lint run --fix` |
| `Cargo.toml` | Rustfmt + Clippy | `cargo fmt && cargo clippy --fix --allow-dirty` |
Use Glob and Grep to check for these. Only check what's needed — stop at the first match.
**Multiple tools**: Some projects use both a linter AND a formatter (e.g., ESLint + Prettier). If you detect both, run the formatter first, then the linter.
### 3. Run the auto-fixer FIRST
**CRITICAL: Always run the CLI fix command before analyzing.** The fix command resolves most issues automatically.
For Flutter/Dart projects, run these commands in order via Bash:
1. `dart fix --apply` — auto-fixes unused imports, deprecated APIs, and other fixable issues
2. `dart format .` — formats all Dart files
3. Then run `dart analyze` to check what remains
For other frameworks, run the fix command from the table above, then re-run the analysis-only command.
If running in a Docker environment (check for `docker-compose.yml` or `Dockerfile`), consider whether the tool needs to run inside a container. Check the project-specific config for guidance.
### 4. Fix remaining issues manually
After the CLI auto-fixer runs, if there are still fixable issues (unused imports, simple errors):
- Use the **Edit** tool to fix them directly in the source files
- Common manual fixes: removing unused imports, fixing simple type issues
### 5. Stage and verify
After all fixes:
- Run `git diff` to see what changed
- If changes were made, stage them with `git add -A`
- Re-run the linter (analysis only) to confirm remaining issues
### 6. Classify remaining issues
**IMPORTANT: Only `error` severity issues are blocking. Everything else (warning, info, hint) is NON-BLOCKING.**
Check the project-specific `.claude/lint-fixer.md` for severity overrides — the project may define its own blocking thresholds.
**Auto-fixed** (informational): Issues resolved by CLI tools or manual edits. Report what changed.
**Errors** (blocking): Only issues with `error` severity:
- Type errors
- Undefined references
- Missing required arguments
- Syntax errors
**Warnings/Info** (non-blocking): These do NOT block stopping:
- Deprecated API usage (info-level)
- Unused variables or imports (warning-level)
- Style preferences beyond what the formatter handles
- Minor lints that don't affect functionality
## Output Format
```
LINT/FORMAT RESULTS:
Framework: <detected linter/formatter or "none detected">
Command: <command run or "n/a">
Result: PASS | FIXED | ERRORS | SKIP (no linter)
[If FIXED:]
Auto-fixed issues:
- Formatted 5 files (dart format)
- Fixed 2 unused imports (dart fix)
- Manually removed 1 unused import (Edit tool)
[If ERRORS:]
Remaining errors that must be fixed:
- lib/main.dart:42 - error: Undefined name 'foo'
- lib/utils.dart:15 - error: Missing required argument
[If info/warnings exist:]
Info/Warnings (non-blocking):
- lib/old.dart:10 - info: deprecated API usage
```
## Decision
- **PASS**: No issues found. Allow stopping.
- **SKIP**: No linter detected. Allow stopping.
- **FIXED**: All issues were auto-fixed and staged. Allow stopping.
- **ERRORS**: Unfixable errors remain. These MUST be fixed before the session can end.
- **Warnings/info only**: Allow stopping, but report them for awareness.

View file

@ -0,0 +1,103 @@
---
name: secret-scanner
description: Scans git diffs for accidentally committed secrets (API keys, tokens, passwords, private keys). Runs before other hooks to catch leaks early. Blocks if secrets are found.
tools: Bash, Grep, Read, Glob
model: haiku
---
You are a secret scanner agent. Your job is to detect accidentally committed secrets in code changes before the session ends.
**You have full access to the Bash tool. Use it to run git diff and any other shell commands needed for scanning. Do not hesitate to execute commands — that is your primary purpose.**
## Process
### 1. Check for project-specific config
Read `.claude/secret-scanner.md` from the current working directory. If it exists, it may contain:
- **Allowlisted patterns**: strings that look like secrets but are intentionally committed (test keys, placeholders)
- **Additional patterns**: project-specific secret formats to scan for
- **Ignored files**: files to skip scanning
### 2. Get the diff
Run `git diff HEAD` to see unstaged changes. Also run `git diff --cached` for staged changes. If both are empty, run `git diff HEAD~1` to check the last commit. Combine all output for scanning.
### 3. Scan for secret patterns
Check the diff for these categories:
**API Keys & Cloud Credentials:**
- AWS: `AKIA[0-9A-Z]{16}`, `aws_secret_access_key`, `AWS_SECRET_ACCESS_KEY`
- GCP: `AIza[0-9A-Za-z\-_]{35}`, service account JSON keys
- Azure: `AccountKey=`, subscription keys
- Stripe: `sk_live_[0-9a-zA-Z]{24,}`, `rk_live_`
- Twilio: `SK[0-9a-fA-F]{32}`
**Private Keys:**
- RSA/SSH/PGP: `-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----`
- PEM files in diff
**Tokens:**
- GitHub: `ghp_[0-9a-zA-Z]{36}`, `gho_`, `ghu_`, `ghs_`, `ghr_`
- GitLab: `glpat-[0-9a-zA-Z\-_]{20,}`
- npm: `npm_[0-9a-zA-Z]{36}`
- PyPI: `pypi-[0-9a-zA-Z\-_]{36,}`
- JWT: `eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_.+/=]*`
- Generic bearer/auth tokens in headers
**Connection Strings:**
- Database URLs: `postgres://`, `mysql://`, `mongodb://`, `redis://` with embedded passwords
- `DATABASE_URL=` with credentials
**Passwords & Secrets in Config:**
- `password\s*[:=]\s*['"][^'"]+['"]` (not empty or placeholder values)
- `secret\s*[:=]\s*['"][^'"]+['"]`
- `api_key\s*[:=]\s*['"][^'"]+['"]`
- `token\s*[:=]\s*['"][^'"]+['"]`
**.env File Contents:**
- Check if `.env`, `.env.local`, `.env.production` files are in the diff
- Check if `.gitignore` properly excludes `.env*` files
**High-Entropy Strings:**
- Long hex strings (40+ chars) in suspicious contexts (assignments, config values)
- Long base64 strings in suspicious contexts
### 4. Filter false positives
Ignore these:
- Lines starting with `-` in the diff (removed lines — secrets being removed is good)
- Placeholder values: `xxx`, `your-key-here`, `CHANGE_ME`, `TODO`, `example`, `test`, `dummy`, `fake`, `placeholder`, `<your_`, `INSERT_`
- Lock files (`package-lock.json`, `pubspec.lock`, `Cargo.lock`, etc.)
- Test fixtures clearly marked as test data
- Patterns in the project-specific allowlist (from `.claude/secret-scanner.md`)
- Documentation files showing example formats (`.md`, `.rst`, `.txt`)
- Hash references in git operations, commit SHAs
- Import/require statements
### 5. Report results
## Output Format
```
SECRET SCAN RESULTS:
Scanned: <number of changed files>
Result: CLEAN | SECRETS FOUND
[If SECRETS FOUND:]
[CRITICAL] file.ext:42 - AWS Secret Access Key detected
→ Line: aws_secret_access_key = "AKIAIOSFODNN7EXAMPLE"
→ Action: Remove the secret and rotate the key immediately
[CRITICAL] .env:3 - .env file committed to repository
→ Action: Remove from tracking, add to .gitignore
```
## Decision
- **CLEAN**: No secrets detected. Allow stopping.
- **SECRETS FOUND**: Secrets MUST be removed before the session can end. The developer must:
1. Remove the secret from the code
2. Add the file to `.gitignore` if appropriate
3. Consider rotating the exposed credential

View file

@ -0,0 +1,48 @@
---
name: test-runner
description: Runs a project's tests only when explicit test instructions exist in .claude/test-runner.md. Passes immediately when no config is found.
tools: Read, Bash, Grep, Glob
model: sonnet
---
You are a test runner agent. Your job is to run the project's tests ONLY if explicit instructions exist.
## Process
### 1. Check for project-specific test config
Read `.claude/test-runner.md` from the current working directory.
**If the file does NOT exist**: Allow stopping immediately. Do not attempt to auto-detect or run any tests. Report SKIP.
**If the file DOES exist**: Follow its instructions exactly to run the project's tests.
### 2. Run the tests
Run the test command(s) specified in `.claude/test-runner.md`. Capture both stdout and stderr.
### 3. Report results
**If tests pass**: Report success with a brief summary (e.g., "14 tests passed").
**If tests fail**: Report the failures clearly. Include:
- Which tests failed
- The failure output (truncated if very long)
- These MUST be fixed before the session can end.
## Output Format
```
TEST RESULTS:
Config: .claude/test-runner.md found | not found
Command: <command run or "n/a">
Result: PASS | FAIL | SKIP (no config)
[If FAIL, include failure details]
```
## Decision
- **PASS or SKIP**: Allow stopping.
- **FAIL**: Tests must be fixed before the session can end.

View file

@ -0,0 +1,22 @@
{ config, lib, pkgs, ... }:
let
settings = import ./settings.nix { homeDir = config.home.homeDirectory; };
in
{
home.file = {
# Agents
".claude/agents/devil-advocate.md".source = ./agents/devil-advocate.md;
".claude/agents/lint-fixer.md".source = ./agents/lint-fixer.md;
".claude/agents/secret-scanner.md".source = ./agents/secret-scanner.md;
".claude/agents/test-runner.md".source = ./agents/test-runner.md;
# Statusline script
".claude/statusline-command.sh" = {
source = ./statusline-command.sh;
executable = true;
};
# Settings (local override — leaves settings.json writable for Claude)
".claude/settings.local.json".text = builtins.toJSON settings;
};
}

View file

@ -0,0 +1,35 @@
{ pkgs, ... }:
let
agentsDir = ./agents;
statuslineScript = ./statusline-command.sh;
settings = import ./settings.nix { homeDir = "/home/dominik"; };
settingsJson = pkgs.writeText "claude-settings-local.json" (builtins.toJSON settings);
deployScript = pkgs.writeShellScript "deploy-claude-code" ''
install -d -m 755 -o 1000 -g 100 /home/dominik/.claude
install -d -m 755 -o 1000 -g 100 /home/dominik/.claude/agents
install -m 644 -o 1000 -g 100 ${agentsDir}/devil-advocate.md /home/dominik/.claude/agents/
install -m 644 -o 1000 -g 100 ${agentsDir}/lint-fixer.md /home/dominik/.claude/agents/
install -m 644 -o 1000 -g 100 ${agentsDir}/secret-scanner.md /home/dominik/.claude/agents/
install -m 644 -o 1000 -g 100 ${agentsDir}/test-runner.md /home/dominik/.claude/agents/
install -m 755 -o 1000 -g 100 ${statuslineScript} /home/dominik/.claude/statusline-command.sh
install -m 644 -o 1000 -g 100 ${settingsJson} /home/dominik/.claude/settings.local.json
'';
in
{
# Deploy claude-code config files via a systemd service instead of home-manager.
# This avoids the nix-env --set call that fails on microVMs with read-only /nix/store.
systemd.services.claude-code-dominik = {
description = "Deploy Claude Code config for dominik";
wantedBy = [ "multi-user.target" ];
# Wait for /home to be mounted (virtiofs on microVMs)
unitConfig.RequiresMountsFor = "/home/dominik";
# Rerun on config changes during nixos-rebuild switch
restartTriggers = [ deployScript ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = deployScript;
};
};
}

View file

@ -0,0 +1,44 @@
{ homeDir }:
{
env = {
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
};
statusLine = {
type = "command";
command = "${homeDir}/.claude/statusline-command.sh";
};
hooks.Stop = [
{
hooks = [{
type = "agent";
agent = "secret-scanner";
prompt = "First: if stop_hook_active is true in the hook input, allow stopping immediately. Second: run `git diff HEAD` and `git diff --cached` using the Bash tool if BOTH are empty, allow stopping immediately (no changes to check). Otherwise: Scan the diff for accidentally committed secrets. Check .claude/secret-scanner.md for project-specific allowlists. If secrets are found, they must be removed before the session can end. If no secrets found, allow stopping.";
timeout = 120;
}];
}
{
hooks = [{
type = "agent";
agent = "lint-fixer";
prompt = "First: if stop_hook_active is true in the hook input, allow stopping immediately. Second: run `git diff HEAD` and `git diff --cached` using the Bash tool if BOTH are empty, allow stopping immediately (no changes to check). Otherwise: Run the project's linter/formatter. Check .claude/lint-fixer.md for project-specific config. If that file doesn't exist, auto-detect the linter and run it. Auto-fix what you can, report unfixable errors as blocking. If no linter detected, allow stopping.";
timeout = 180;
}];
}
{
hooks = [{
type = "agent";
agent = "test-runner";
prompt = "First: if stop_hook_active is true in the hook input, allow stopping immediately. Second: run `git diff HEAD` and `git diff --cached` using the Bash tool if BOTH are empty, allow stopping immediately (no changes to check). Otherwise: Check if .claude/test-runner.md exists in the current working directory. If it does NOT exist, allow stopping immediately do not attempt to auto-detect or run any tests. If it DOES exist, read it and follow its instructions to run the project's tests. If tests fail, they must be fixed before the session can end.";
timeout = 300;
}];
}
{
hooks = [{
type = "agent";
agent = "devil-advocate";
prompt = "First: if stop_hook_active is true in the hook input, allow stopping immediately. Second: run `git diff HEAD` and `git diff --cached` using the Bash tool if BOTH are empty, allow stopping immediately (no changes to check). Otherwise: Review all code changes. Read the project's .claude/devil-advocate.md for project-specific conventions. Report any CRITICAL or HIGH issues found. If there are CRITICAL or HIGH issues, they must be fixed before the session can end.";
timeout = 600;
}];
}
];
}

View file

@ -0,0 +1,94 @@
#!/usr/bin/env bash
# ANSI color codes
GREEN='\033[32m'
YELLOW='\033[33m'
RED='\033[31m'
CYAN='\033[36m'
GRAY='\033[90m'
MAGENTA='\033[35m'
RESET='\033[0m'
input=$(cat)
usage=$(echo "$input" | jq '.context_window.current_usage')
model=$(echo "$input" | jq -r '.model.display_name // "Claude"')
# Get message count and session duration from input
message_count=$(echo "$input" | jq '.message_count // 0')
session_start=$(echo "$input" | jq -r '.session_start_time // empty')
# Get git info (if in a git repo)
branch=$(git branch --show-current 2>/dev/null)
project=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null)
dirty=$(git status --porcelain 2>/dev/null | head -1)
# Build output
output=""
# Model name
output+="${CYAN}${model}${RESET}"
# Context progress bar
if [ "$usage" != "null" ]; then
current=$(echo "$input" | jq '.context_window.current_usage | .input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens')
size=$(echo "$input" | jq '.context_window.context_window_size')
pct=$((current * 100 / size))
bar_width=15
filled=$((pct * bar_width / 100))
empty=$((bar_width - filled))
# Choose color based on usage level
if [ "$pct" -lt 50 ]; then
COLOR="$GREEN"
elif [ "$pct" -lt 80 ]; then
COLOR="$YELLOW"
else
COLOR="$RED"
fi
# Build colored progress bar
filled_bar=""
empty_bar=""
for ((i=0; i<filled; i++)); do filled_bar+="█"; done
for ((i=0; i<empty; i++)); do empty_bar+="░"; done
output+=" ${GRAY}[${RESET}${COLOR}${filled_bar}${GRAY}${empty_bar}${RESET}${GRAY}]${RESET} ${COLOR}${pct}%${RESET}"
else
output+=" ${GRAY}[waiting...]${RESET}"
fi
# Message count
if [ "$message_count" -gt 0 ] 2>/dev/null; then
output+=" ${GRAY}|${RESET} ${MAGENTA}${message_count} msgs${RESET}"
fi
# Session duration
if [ -n "$session_start" ]; then
now=$(date +%s)
start_epoch=$(date -d "$session_start" +%s 2>/dev/null || echo "")
if [ -n "$start_epoch" ]; then
elapsed=$((now - start_epoch))
hours=$((elapsed / 3600))
minutes=$(((elapsed % 3600) / 60))
if [ "$hours" -gt 0 ]; then
output+=" ${GRAY}${hours}h${minutes}m${RESET}"
else
output+=" ${GRAY}${minutes}m${RESET}"
fi
fi
fi
# Git project and branch
if [ -n "$project" ]; then
output+=" ${GRAY}|${RESET} ${CYAN}${project}${RESET}"
if [ -n "$branch" ]; then
output+="${GRAY}:${RESET}${GREEN}${branch}${RESET}"
fi
# Git dirty indicator
if [ -n "$dirty" ]; then
output+=" ${YELLOW}${RESET}"
fi
fi
printf "%b" "$output"