2026-07-01 Dotfiles IPython Config Fixes and Claude Attribution Settings

Context: Cloud Claude Code session on dotfiles (github.com/achhina/dotfiles). Started as a one-line bug report about a broken IPython version guard and grew into two merged PRs (#1, #2) covering the guard fix, an IPython 9 theming migration, and Claude Code attribution settings for both local and cloud sessions.

What I set out to do

Fix the IPython version guard in nix/home-manager/modules/ipython.nix: it checked hasattr on c.TerminalInteractiveShell, which is a traitlets Config section, not the shell class — and config sections auto-create any attribute you touch, so the guard was unconditionally true.

What I actually did

  • PR #1 — guard fix (merged). First pass imported the real TerminalInteractiveShell class and guarded with hasattr on it. Then tightened to 'auto_match' in set(_TIS.class_traits(config=True)), which checks the exact condition that matters: the trait exists and is settable via config.
  • PR #2 — three follow-ups (merged).
    • Migrated highlighting_style (deprecated no-op since IPython 9.0) to the IPython 9 theme system: c.InteractiveShell.colors = "gruvbox-dark" on 9+, old pygments style + colors = "Linux" on <9.
    • Replaced deprecated includeCoAuthoredBy = false in claude.nix with the attribution = { commit = ""; pr = ""; } object.
    • Added a repo-level .claude/settings.json with attribution (incl. undocumented sessionUrl: false) so cloud sessions on this repo also skip attribution trailers.
  • Commit authorship cleanup. The cloud container’s git identity was Claude <noreply@anthropic.com>; rewrote the PR #1 commits as Adam Chhina <amanschhina@gmail.com> (byte-identical trees, trailers stripped) and set repo-local git config so later commits were mine from the start.
  • All changes verified against a real IPython 9.15.0 in a venv by rendering the full generated ipython_config.py and launching ipython against it — clean startup, theme applied, guard working.

What was striking

  • Traitlets Config.__getattr__ auto-vivifies: any unknown key returns a fresh empty Config, so hasattr on a config section can never be false. The classic footgun the original guard fell into — while the ruff-formatter block right next to it got it right against the imported class.
  • hasattr on the class is still too loose. Names like system, mainloop, debugger_cls pass hasattr(TerminalInteractiveShell, ...) but aren’t configurable traits — setting them via c. triggers “config option not recognized” warnings. class_traits(config=True) is the precise, self-documenting guard.
  • IPython 9 ships gruvbox-dark as a built-in theme (IPython.utils.PyColorize.theme_table), so the deprecated pygments style mapped 1:1 with no visual downgrade.
  • Last write wins on a trait. The smoke test caught that the unconditional c.InteractiveShell.colors = "Linux" later in the file silently overrode the theme — it had to move into the else branch. The bug first appeared in my test render, which was itself a reminder that verification scripts can have bugs too.
  • User ~/.claude/settings.json does not carry over to Claude Code cloud containers — but a project-scoped .claude/settings.json in the repo does, since the clone is fresh. There’s no app-level setting. The Claude-Session: URL trailer is a separate injection with only an undocumented toggle (attribution.sessionUrl, anthropics/claude-code#69614).
  • Force-push lease went stale after PR #1 merged because GitHub deleted the remote branch while the local tracking ref still pointed at it — git remote prune origin fixed it.

Top 3 next

  1. Run hm switch locally to apply the claude.nix change and confirm the attribution key passes the module’s settings schema (couldn’t run nix in the cloud container).
  2. Consider a SessionStart hook (or setup script) that sets git user.name/user.email in cloud containers so commits are authored correctly without post-hoc rewriting.
  3. Replicate the .claude/settings.json attribution block in other repos used with Claude Code on the web — it’s per-repo, not global.