EVBXGRCFNANXS3OW6CEFN433ZCMDCPXPO2ZTVAYXARZYLRKCQE5AC MNNZDFFI5VLYOVFD4SDFAA2NTLWOU46HTMRKA5QI3RNXRFKQB42QC EZF3C4UGL7QUDQI567LCKLU4FCP3R64SLCPVIRBYIS7Q74HGRXMQC MOQV2NY4ETD5D727YAFZNDIGSD6OVGEEDSCS2K5G6G2TOKI5B2HQC 5YZRDAU4ABMFJIJEBGDRMLQKNHIOC22OK2ZJ4MOI4NELAGVVK5ZQC AHAA7UNL2RRXP7KERGVCC42GB564WS5BYSSFEOXW3EKVMB2UVEOAC CBGXBY2CSVY6SK5KLBIHTWKDVUKXZIB5EXJKULHP75QGOMH6G2WAC 53IQQGOH2GVM745ZEKO4S6RPSQG4UZH6J5G7WUKO4ZCP22D3IO6QC TSX4OAMHBTCC3T6IK3HDNQYKI2GTW7PG7XFAGI2HOKIVLXK42G4AC 4TIHVAKKNUZV5IL5YUIQD7JXQ32PPYUT3IRJUSAI46UM2EKSLPLQC YFWDBAWXXEYS2Q2JAAL56B3NDNC4AV5KFNM3VPH6OXNKEOX6RDKAC USORQI4YM2FOILPSRJ444BEV6APPCI76CLGI5ZBDBOGCWAUALEQQC ICWY2FLS75I6PRBQYSHNA5HJHGNL3CMWGI66XW7XMKJAVL3LYMJAC 3ZG2UVJL4R4YYAOVPHQD3PHYAGKUZHIT7NSCHXPIPDPOQPUTDLAAC 4VOAN6JG5TRYVGJFWW5I5LCN6HVFTOL7KVJ2RBP44F44EI4ATHBQC ILAGTYHND3Y4JI7DKCWYHVTSH67TP72LJV6ZDTOROUTQDIIIAM4AC 46RUDLGYFOIQKDS7WWE5U6TIMYO3XY6IIIJI55HU3RXYORIVYVXQC RWWWW4A6Q6LX6ZB742GFTTWYQJ2DHPLL2SJKEX5L735JXXSIEPLQC PAC4TJKFSSQAOZDUM5O7KJMZADW72H4UMI6C7DYFXKGAWJEY6CDAC IRXRFGYCQCHY6YANFQUWC2COCHNTWW3V4OWN4LCMXNFDE6OQTO5QC GOKVITZZNI4HOXK7WUBCCKWE7C23RVGMWMGMBLESWKWLHPGTLHWAC KSUIUTK56M2DPFUEK5BQ6TPJL7546ADTOI3XXAAFHEMDXPYYUFYAC MH3526BUZ3JHPCJ2NOL2W7IZHRIM3JHSILRO3FD2TOWLW6S7D7KQC QJOSU3BH5HVJAWRFWSWTVSSDLXOQPIZO7LXHH2DJ44XJMWZPUXVAC UXKPHE3UG2HUJ7SQUCMU7VU2V75634ZJQDH4RJOGX2JHOW2PPBBAC 5G7WRBMWKG6DMCOHE6WQHTYZACUHO2UPBZRWN72CFH7P45NN5E7QC HNXQ2AG6SV2VEE5RZG3JLWQ24P2YUJWHJKMP2GMYT2AS2HZGP72QC RD44H3Z4QK3LSNHNIS6N4DAEWIW7BNUF3S552JQOFXRRM3VQ5SAAC Q2XLHBS67AEUCGEJPDR7HCWTVIYKBDNW7XBK4WN2NK4BHUNCRAOQC RXJH46XLP6AFSIZZD4WEQA5A5YZLKHZ54ZUGCRH7TRAOH2Z4Q2VQC RNQOZELE32XPE36GFN2I6P54AEV37ICKNRLGQF2JZ7WY2ZUKB57AC LNEFSFVU4YSTF7V36E24OWJN7ZLB4H2ZQS7K3NYIZ2D6H32CWCTAC PS5CLGUA4IIS735LJN3MWW66PHDDNDW5ZUE4N2TYL3ACO33OHNUAC RK6HMH57O4DF52G5OBASU3Q3ZI7IYBAGONCSBHL2AOBMCT54W3SQC PEJMIAQKMRMK6YZHLNQUTT3T4EQGUUYUQVPYMEYKIDNVYEX566DAC 4K7JJXF72TXYREA33P6L3AJPAEBMJPT7U44TUW7UGH57Z4HVCEXQC Z6CX7K6OFYRBWLIMPIKYXJHIIYL54YFUPMPUGVTR5WE2PJNXYSQQC 7V33OASZUNI6Q3NHNCFUJABI6DFPQPYPTVLD4Z3ZUAAVKNJ7JMPQC M7G7QTQ3CNO46H52V4JVUXC6B6PVAWTOEOJUIEUYAGFOKKV2ANGQC JBEMS7VF7BR2NU7IJCKTHURZ5K5365X7RETK6H2LD6ZJDNL6WMRQC IXYHE6G2UPPHJH76ZYKIJGHQKTRT327AI4DD5RNRPVKO2RPH7WQAC JRMAVTESBBAHOESMTEE46DVSWCQEWACRDKLZ3GNUWAU4G345HH4AC XY5XYQTQY3LK5VRIOQVXWSBJYLLWFRZTU6I535JA66O2MM64QESAC HKY4MG5EED2AQLLAF7MFX6QVMANS5YKDILHHKIGY2RFCHSS5HS6AC UYE3457WQC2CN7Q6LLYCRYJHBAM5NSGCMV33OTUTT2UBUXDR5FSQC EHH4VW45MDSTBOAM7YKT6MO24CFVB2XM2LPTHDIIMLV4YVGLRS6AC TQ2OMV4DGTIGJIFOKWYKZQRTW3FXYGXI2VLJBY56U4XLHHUCY7WQC B4V56AJ7GG7XCZJ5XESESDG5TGZ4VKEO7467X2LHWURLBALWVKKQC NJTZJVPA5P7XCGA3JTRAHSHOEG3VJPCDAHOSHM3BP776EP4OMIBQC YYT732Q74ZLHZE6FRD6U3KD3EWI4T5GPCBL75ZDKM62GCX47X4NQC H6C4OXGJGGHYIXZ63XUJ4GCUIBMTXEVSZQSRAPYVP2HET4K35BCQC YOZWGC4FRPLIWT4BGHLMW6Q2Y2KVZCPXKTFFB4WDZJPUSO3ZWQXAC SQOORYTI4I7PKQSJVGX4UDHCV6Z6SCI6NNMCPXMMVUZB54L7RTAQC 5ZFZI6J6MFVQWIB4D3ELCTZILHLDSSAUAB7RMF4JJ7VAX3XOLAVAC 6KRI7YWKGAOUKBQJH64TC5DJ6VRTPDUJYOWMGZAOEU2ME2VOZI2AC IYDRQRI2CS5H4YPCIKZTEI5OAQMATAOEGAF2WDE4W3W6IMS5SAFQC FLU5PBUFR5SYVA3D2GSAAEMFVBZV5RHMUUJICQEY4STBC4H43DQQC OIB6XTAUJJXK7IBESGFZS34O6X6F2H2YXKFCX4UAPK6MNVISI4FQC VRBR7LPFLNJDYYRHLHO3VPLJEQIDF4QOFNDMUPZNTIJN5TSGKVXAC WOOUWJJQSIIHV6LUI5WMCRW72522AGQ2DXWXJHUJDZX6GCGR24AQC Y3ASW44YHRCO3ZMCEBO2I7GQEDU2SL4SM3PXJCBCOZ4MYXU4ESPQC JVRZFFCJX6YEDXVOMKGMSUKTKEOO2S7UCJQVBTVSPPSWBCFENWCQC SE73WILK2MCH43XREZANM73UFAYT2O7YB2YRFB2WYFD5ZPFE2Q5AC 37INZQH34IZXE5F7I54LO33OL2EAIVQ2J5ZR72BMG3K7TR7AUBSQC MZ64CKVYIZTIHIYNKUG53NRPNDSLGFVFVLMOJWU4ZMEFT2ADPTYQC BE4NH5IASMF5PMZ3D7YZI5XZ56FHJ7A3726FZHB3CF4PBMRUTHHAC E5WANV7PYNPZFSJE4AMZABQ76TUNKVZN3M6ZFD5V6CKAFKG444NAC UU7XD2JOSJ3SB7W2LZRGHEW5T6BZ272ZU36R4PV4733SEHXJ3WKAC 37OEQONTL4UWOCKL4S7HAHZ3L5OFHCNIBYNOF7T5IARZXSTFK4DQC GG6IGPT2AA5ANGQDQFTGQTHXV3HCRGXZ7H56F7O3HUARTG5YZVXAC MIO6E4WROXK3LZSMQU5VE2ADX2WJZZVHLW5PLMDPUMFBT7X5SBVAC B25Y6ZESZM3UB3G7F2QCANOQN6FH5O4A5G7SFUJ5LLWWR7DZNHDQC YKOPXUNRLG6IIZVLDLOBIOEXZWLQCTZ7G2BTSDL2BJUYVTD6UPKAC claudeCodePackage = pkgs.symlinkJoin {name = "claude-code-wrapped";paths = [ inputs.claude-code.packages.${pkgs.stdenv.hostPlatform.system}.default ];buildInputs = [ pkgs.makeWrapper ];postBuild = # sh''wrapProgram $out/bin/claude \--run 'export ANTHROPIC_AUTH_TOKEN=$(cat ${secrets.zaiKey.path})''';};
# claude-code sandboxing deps.# pkgs.socat# pkgs.bubblewrapclaudeCodePackage
# TODO: Add claude-code and opencode config files.files.".claude/settings.json" = {generator = toJSON { };value = {cleanupPeriodDays = 1000;alwaysThinkingEnabled = false;includeCoAuthoredBy = false;statusLine = {type = "command";command = "python3 ~/.claude/scripts/statusline.py";};sandbox = {enabled = false;autoAllowBashIfSandboxed = true;excludedCommands = ["git""jj""docker""just"];allowUnsandboxedCommands = true;network.allowUnixSockets = [ "/run/user/1000/docker.sock" ];network.allowLocalBinding = true;};env = {ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic";API_TIMEOUT_MS = 3000000;ANTHROPIC_DEFAULT_HAIKU_MODEL = "glm-4.7-flash";ANTHROPIC_DEFAULT_SONNET_MODEL = "glm-5";ANTHROPIC_DEFAULT_OPUS_MODEL = "glm-5";CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR = 1;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = 1;CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = 1;DISABLE_NON_ESSENTIAL_MODEL_CALLS = 1;};hooks = {# Unreliable right now: https://github.com/anthropics/claude-code/issues/11947# Stop = [{# hooks = [{# type = "prompt";# prompt = ''You are evaluating whether Claude should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed.'';# timeout = 30;# }];# }];# SubagentStop = [{# hooks = [{# type = "prompt";# prompt = ''Evaluate if this subagent should stop. Input: $ARGUMENTS\n\nCheck if:\n- The subagent completed its assigned task\n- Any errors occurred that need fixing\n- Additional context gathering is needed.'';# }];# }];Notification = [{matcher = "permission_prompt|elicitation_dialog";hooks = [{type = "command";command = "${pkgs.libnotify}/bin/notify-send --expire-time=15000 'Claude' 'Waiting for approval.'";}];}{matcher = "idle_prompt";hooks = [{type = "command";command = "${pkgs.libnotify}/bin/notify-send --expire-time=15000 'Claude' 'Waiting for next message.'";}];}];PostToolUse = [{matcher = "Edit|MultiEdit|Write";hooks = [{type = "command";command = "~/.claude/hooks/format-files";}];}];};permissions = {allow = ["Edit(PROJECT.md)""Edit(CURRENT.md)""Edit(STATE.md)""Update(PROJECT.md)""Update(CURRENT.md)""Update(STATE.md)""Bash(curl http://localhost:*)""Bash(curl -X GET http://localhost:*)""Bash(find:*)""Bash(rg:*)""Bash(fj issue search:*)""Bash(fj issue view:*)""Bash(fj actions tasks:*)""Bash(fj wiki contents:*)""Bash(fj wiki view:*)""mcp__context7""mcp__gh_grep""mcp__web-reader""mcp__web-search-prime""mcp__nixos"];deny = ["Read(*.env)""Read(*.envrc)""Bash(git push:*)""Bash(git commit:*)"];};mcpServers = {gh_grep = {type = "http";url = "https://mcp.grep.app";};# No support for reading secrets from files yet. These are added with# `~/.claude/claude-mcps.sh` instead.# context7 = {# type = "http";# url = "https://mcp.context7.com/mcp";# headers = {# # We need this for higher limits but for now it's fine and doesn't stop us using it.# CONTEXT7_API_KEY = "{file:${secrets.context7Key.path}}";# };# };## web-reader = {# type = "http";# url = "https://api.z.ai/api/mcp/web_reader/mcp";# headers = {# Authorization = "Bearer {file:${secrets.zaiKey.path}}";# };# };## web-search-prime = {# type = "http";# url = "https://api.z.ai/api/mcp/web_search_prime/mcp";# headers = {# Authorization = "Bearer {file:${secrets.zaiKey.path}}";# };# };
nixos = {type = "stdio";command = "/run/current-system/sw/bin/nix";args = ["run""github:utensils/mcp-nixos""--"];};playwriter = {type = "stdio";command = "/run/current-system/sw/bin/npx";args = [ "playwriter@latest" ];};};};};
# Create hooks with home-manager to avoid permissions issues.files.".claude/hooks/format-files" = {text = # nu''#!/usr/bin/env nulet json_input = (^cat)let file_path = ""if (which jq | is-empty) { exit 2 }let $file_path = ($json_input | from json | get tool_input.file_path)if ($file_path | is-empty) { exit 2 }let extension = ($file_path | path parse | get extension)let command = match ($extension | str trim) {"rs" if (which cargo | is-not-empty) => { ["rustfmt" $file_path] }"toml" if (which taplo | is-not-empty) => { ["taplo" "fmt" $file_path] }_ => {print "This file extension is not covered by this script"exit 0}}^$command.0 ...($command | skip 1)'';executable = true;};# Statusline script.files.".claude/scripts/statusline.py" = {text = # py''#!/usr/bin/env python3"""PlumJam's Claude Code Statusline<https://git.plumj.am/plumjam/nixos/src/branch/master/modules/ai.nix>"""import jsonimport sysimport osimport reimport timedef parse_context_from_transcript(transcript_path):if not transcript_path or not os.path.exists(transcript_path):return Nonetry:with open(transcript_path, 'r', encoding='utf-8', errors='replace') as f:lines = f.readlines()# Check last 15 lines for context information.recent_lines = lines[-15:] if len(lines) > 15 else linesfor line in reversed(recent_lines):try:data = json.loads(line.strip())# Method 1: Parse usage tokens from assistant messages.if data.get('type') == 'assistant':message = data.get('message', {})usage = message.get('usage', {})if usage:input_tokens = usage.get('input_tokens', 0)cache_read = usage.get('cache_read_input_tokens', 0)cache_creation = usage.get('cache_creation_input_tokens', 0)# Estimate context usage (assume 200k).total_tokens = input_tokens + cache_read + cache_creationif total_tokens > 0:percent_used = min(100, (total_tokens / 200000) * 100)return {'percent': percent_used,'tokens': total_tokens,'method': 'usage'}# Method 2: Parse system context warnings.elif data.get('type') == 'system_message':content = data.get('content', "")# "Context left until auto-compact: X%"match = re.search(r'Context left until auto-compact: (\d+)%', content)if match:percent_left = int(match.group(1))return {'percent': 100 - percent_left,'warning': 'auto-compact','method': 'system'}# "Context low (X% remaining)"match = re.search(r'Context low \((\d+)% remaining\)', content)if match:percent_left = int(match.group(1))return {'percent': 100 - percent_left,'warning': 'low','method': 'system'}except (json.JSONDecodeError, KeyError, ValueError):continuereturn Noneexcept (FileNotFoundError, PermissionError):return Nonedef get_context_display(context_info):if not context_info:return f"\033[38;5;109m━┫ ┣━ 0%\033[0m"percent = context_info.get('percent', 0)warning = context_info.get('warning')# Create progress bar.segments = 10filled = int((percent / 100) * segments)bar = "█" * filled + " " * (segments - filled)return f"\033[38;5;109m━┫ [{bar}] ┣━{percent:.0f}%\033[0m"def get_directory_display(workspace_data):current_dir = workspace_data.get('current_dir', "")project_dir = workspace_data.get('project_dir', "")if current_dir and project_dir:if current_dir.startswith(project_dir):rel_path = current_dir[len(project_dir):].lstrip('/')return rel_path or os.path.basename(project_dir)else:return os.path.basename(current_dir)elif project_dir:return os.path.basename(project_dir)elif current_dir:return os.path.basename(current_dir)else:return "unknown"def get_session_duration(duration_ms):if duration_ms > 0:minutes = duration_ms / 60000if minutes < 1:return f"{duration_ms//1000}s"else:return f"{minutes:.0f}m"return ""def main():try:# Read JSON input from claude-code.data = json.load(sys.stdin)model_name = data.get('model', {}).get('display_name', 'Claude')workspace = data.get('workspace', {})transcript_path = data.get('transcript_path', "")cost_data = data.get('cost', {})thinking_enabled = os.environ.get('THINKING_ENABLED', "").lower() in ('true', '1', 'yes')context_info = parse_context_from_transcript(transcript_path)context_display = get_context_display(context_info)directory = get_directory_display(workspace)duration_ms = cost_data.get('total_duration_ms', 0)session_duration = get_session_duration(duration_ms)thinking_indicator = " \033[38;5;172m|\033[0m \033[38;5;142mTHINKING\033[0m " if thinking_enabled else " "# Combine all components in the correct format.status_line = f"\033[38;5;142m[{model_name}]\033[0m \033[38;5;214m{directory}\033[0m{thinking_indicator}{context_display}"if session_duration:status_line += f" \033[38;5;172m|\033[0m \033[38;5;214m{session_duration}\033[0m"print(status_line)except Exception as e:# Fallback display on any error.print("error")if __name__ == "__main__":main()'';};# Helper script to add MCPs.files.".claude/claude-mcps.sh" = {text = # bash''#!/usr/bin/env bash# Run this once to add the MCP servers that need API keysclaude mcp add -s user -t http context7 https://mcp.context7.com/mcp --header "CONTEXT7_API_KEY: $(cat ${secrets.context7Key.path})"claude mcp add -s user -t http web-reader https://api.z.ai/api/mcp/web_reader/mcp --header "Authorization: Bearer $(cat ${secrets.zaiKey.path})"claude mcp add -s user -t http web-search-prime https://api.z.ai/api/mcp/web_search_prime/mcp --header "Authorization: Bearer $(cat ${secrets.zaiKey.path})"claude mcp add -s user -t http zread https://api.z.ai/api/mcp/zread/mcp --header "Authorization: Bearer your_api_key"'';executable = true;};
"type": "github"}},"claude-code": {"inputs": {"flake-utils": "flake-utils","nixpkgs": ["os"]},"locked": {"lastModified": 1771013219,"narHash": "sha256-EyR1pOL4vWyefDij3/HyZd3qjG6gzfk2BcjbAFQiXdY=","owner": "sadjow","repo": "claude-code-nix","rev": "3f8cff15b530cc4f09ed2497932a66662f7a8422","type": "github"},"original": {"owner": "sadjow","repo": "claude-code-nix",
"flake-utils_4": {"inputs": {"systems": "systems_6"},"locked": {"lastModified": 1731533236,"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=","owner": "numtide","repo": "flake-utils","rev": "11707dc2f618dd54ca8739b309ec4fc024de578b","type": "github"},"original": {"owner": "numtide","repo": "flake-utils","type": "github"}},
"locked": {"lastModified": 1681028828,"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=","owner": "nix-systems","repo": "default","rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e","type": "github"},"original": {"owner": "nix-systems","repo": "default","type": "github"}},"systems_7": {