2026-06-06 Untangling the update -c Failure Cascade

What I set out to do

Fix one error: update -c died on nix flake update with error: creating pipe: Too many open files. Expected a quick ulimit bump.

What I actually did

It turned into a five-layer cascade, each layer only visible once the one before it was cleared:

  1. File-descriptor limit (the reported bug). macOS launchd hands processes a 256 open-file soft limit (launchctl limit maxfiles), and nix flake update fans parallel fetches/pipes across every transitive input until it exhausts that. The trap: every shell I could spawn reported ulimit -n = 1048576, because Claude Code runs on Node and Node auto-raises RLIMIT_NOFILE at startup, masking the real terminal limit. Had to reason about it via launchctl, not ulimit. Fix: update self-raises to 65536, and ~/.config/zsh/.zshrc raises it for all interactive shells.
  2. litellm patch broke. The nixpkgs bump pulled litellm 1.83.14 → 1.86.0, and my litellm-25240-responses-custom-llm-provider patch lost 2 of 6 hunks. Confirmed it was a real patch failure (not a Bash truncation/timeout) by rebuilding the derivation in isolation. Regenerated the patch against the 1.86.0 source by diffing a real copy; the build’s own 9-marker assertion (3 pristine + 6 patched) passed.
  3. settings.json collision. HM refused to clobber ~/.claude/settings.json (now a plain file, not the symlink). Diffed it against the generated output: every difference was a /nix/store hash drift, no real edits, so safe to reclaim.
  4. prek vs git-hooks.nix. The commit step failed with prek “migration mode”. Root cause: the devshell shellHook ran config.pre-commit.installationScript, installing git-hooks.nix’s classic shim over prek’s on every entry. Per Nix - Home Manager and ADR 0005, git-hooks.nix only generates the config and prek owns execution, so the shellHook now does prek install -f. Proved idempotent by planting a foreign shim and re-entering.
  5. Commit hygiene. The script’s bare git commit had swept my fix files into the lock-bump commit. Split history into atomic commits, then fixed the script itself to pathspec-scope its commits (git commit -- <lock>) so it can’t recur.

What was striking

  • The masking effect of Node raising the fd limit was the trickiest part. The diagnostic tool’s environment lied about the symptom. Lesson reinforced: inspect the live system at the right layer (launchctl), don’t trust the convenient readout.
  • “Is it failing or just timing out?” was a good challenge. Reproducing the litellm build in isolation was the right way to answer it with evidence rather than assertion.
  • One reported error, five real bugs. Flake updates surface latent coupling (version-pinned patches, plain-file drift, hook-installer conflicts) all at once.