Skip to content

Repository files navigation

dotdotdots

Mes dotfiles, sur deux machines :

Machine OS Desktop Profil
pro Arch + Omarchy Hyprland omarchy
perso Pop!_OS 24.04 COSMIC popos

Le terminal, le thème et les outils CLI sont identiques sur les deux. Le desktop ne l'est pas, et n'essaie pas de l'être.


Installation

git clone https://github.com/brieucdlf/dotdotdots ~/.dotfiles
cd ~/.dotfiles && ./install.sh

En HTTPS, et pas en SSH — l'ordre est contraint. Cloner en SSH exigerait la clé sk, que ssh-keygen -K régénère depuis la YubiKey, ce qui exige libfido2, qu'installe install.sh, qui est dans le dépôt qu'on cherche à cloner. La boucle ne s'ouvre que par HTTPS : le dépôt est public, la lecture anonyme ne demande rien. Une fois les clés régénérées (voir Sur une seconde machine), on bascule le remote :

git remote set-url origin git@github.com:brieucdlf/dotdotdots

C'est le sens de la bascule qui compte : lire n'a jamais besoin d'identité, écrire en exige une. Rien d'autre dans ce dépôt ne doit dépendre d'un secret pour être installé — sans quoi il cesse d'être ce qui suffit à refaire une machine.

Le script détecte le profil, installe les paquets manquants, stow les paquets, rend le thème et lance mise install. Il est idempotent — relançable à volonté.

Il déblaie aussi le terrain avant de stower, parce qu'un seul conflit fait avorter stow en entier et laisse la machine sans ancienne config ni nouvelle :

  • tout ce qui existe déjà et bloquerait stow — vrai fichier, mais aussi lien posé par un autre outil, sur un fichier comme sur un dossier — part dans ~/.dotfiles-backup/<horodatage>/, jamais écrasé ;
  • les liens de ~ qui pointent dans le repo vers un chemin disparu (après un renommage, par exemple) sont retirés : ils ne contiennent rien.

Si stow refuse quand même, le script s'arrête en disant où sont les fichiers écartés — plutôt que de mourir sur l'erreur brute de stow.

./install.sh --profile popos   # forcer un profil
./install.sh --no-packages     # stow + thème seulement

Les trois couches

Architecture des dotfiles : colors.toml et common/ alimentent install.sh, qui n'applique que le profil correspondant au système détecté

La règle qui tranche : si ça tourne dans un terminal → common/. Si ça dépend du compositeur ou du gestionnaire de paquets → un profil.

common/            # les 2 machines, identique au byte près
├── .bashrc                     # loader : Omarchy si présent, puis init.bash
├── .gitconfig
├── .config/
│   ├── bash/init.bash          # socle shell indépendant de la distro
│   ├── bash/{exports,aliases,work}.bash
│   ├── ghostty/  tmux/  nvim/  starship.toml
│   ├── mise/config.toml        # ← versions des outils, la clé de l'iso
│   └── zed/
├── .claude/                    # config Claude Code (settings, skills, statusline)
└── .local/bin/                 # claude-panel, tmux-claude-status, dots-shell-dump

theme/
├── nurburgreen/    # colors.toml = SOURCE UNIQUE des couleurs (+ ui.toml)
├── cosmic/         # palette sémantique stock de COSMIC, reprise telle quelle
└── render.sh       # → ~/.config/theme/current/{ghostty,tmux,colors.sh,*.ron}

omarchy/            # hypr/, waybar/
popos/              # thème COSMIC + override ghostty (voir son README)

common/ et le profil sont stowés en une seule invocation (stow --restow common $PROFILE) : traités séparément, stow refuse de déplier un dossier posé par l'autre passage.


Thème Nurburgreen

Construit autour d'une Porsche 911 GT3 en British Racing Green.

background  #001a0f   BRG dark
foreground  #d4b88a   cognac leather
accent      #f0c000   GT yellow (used sparingly)

theme/nurburgreen/colors.toml est la seule source de vérité. theme/render.sh en dérive les fichiers consommés par ghostty, tmux, fzf et eza dans ~/.config/theme/current/aucune couleur n'est codée en dur dans une config. Les tokens qui n'existent pas dans le format Omarchy (bordures, fond de barre) sont dans ui.toml, à côté.

Changer une couleur = éditer colors.toml, relancer ./install.sh --no-packages.

Sur la machine Omarchy, install.sh lie aussi le thème dans le sélecteur Omarchy pour que Waybar et Hyprland le voient. Le terminal, lui, ne dépend pas d'Omarchy : c'est ce qui rend l'iso vraie.

Sur Pop!_OS, le desktop COSMIC a droit au même traitement : render.sh produit un cosmic-nurburgreen-dark.ron importable via Réglages > Apparence > Importer un thème. Détail du mapping dans popos/README.md.


Iso des outils

common/.config/mise/config.toml fixe les versions de tout le socle CLI (eza, fzf, zoxide, starship, delta, lazygit, fd, rg, btop, bat, gh, jq, neovim) en plus des runtimes. Un mise install et les deux machines ont les mêmes binaires, indépendamment d'apt et de pacman.

⚠️ Sur la machine Omarchy, les shims mise passent devant les binaires pacman. C'est voulu.

Restent hors mise, installés par install.sh : ghostty (.deb mkasberg/ghostty-ubuntu sur Pop, pacman sur Arch), tmux, stow et la JetBrainsMono Nerd Font.

Vérifier que les deux machines ont convergé

Le socle shell vient d'Omarchy sur la machine pro et de init.bash sur Pop : rien ne garantit qu'ils soient identiques. Pour rendre l'écart visible :

dots-shell-dump > ~/shell-$(hostname).txt   # sur chaque machine
diff ~/shell-pro.txt ~/shell-perso.txt

Ce qui manque côté Pop est à recopier dans common/.config/bash/init.bash.


Secrets

Les clés API et l'identité pro sont chiffrées dans le repo, et ne s'ouvrent qu'avec une YubiKey. La clé privée age est générée dans l'applet PIV du token et n'en sort jamais : il n'y a rien à sauvegarder ailleurs, et un vol de machine ne donne accès à rien.

C'est ce qui ferme la boucle du reset — avant, un poste neuf retrouvait un shell parfait mais aucune clé, et il fallait les recopier à la main depuis on ne sait où.

dots-secrets status            # état de la chaîne
dots-secrets verify            # prouver que la clé branchée ouvre tout
dots-secrets edit local.bash   # éditer un secret (le clair reste en tmpfs)
dots-secrets unseal            # (re)déposer les secrets sur la machine
dots-secrets enroll            # enrôler une YubiKey de plus

./install.sh déchiffre tout seul si la clé est branchée, et se termine quand même sinon. Détail du modèle, ajout d'un secret, perte d'une clé : secrets/README.md.

ykman est installé avec le reste : c'est le seul outil qui inspecte et reconfigure les applets du token — lister les identifiants résidents, remettre un PIN, lire le numéro de série. Rien ici n'en dépend au quotidien, mais sans lui un token qui se comporte mal est une boîte noire.

Il sert notamment à retirer l'applet OTP si elle n'est pas utilisée (ykman otp delete 1). Cette applet tape une longue chaîne à chaque touche reçue hors d'une demande FIDO — donc dans un terminal, un champ de mot de passe ou une messagerie, selon ce qui a le focus. Sur un token qui ne sert qu'au FIDO et au PIV, elle ne fait que transformer une touche mal placée en accident au lieu d'un non-événement.

La chaîne ne sert pas qu'aux fichiers de configuration. todo-secu y range les faiblesses connues et non corrigées — une liste de trous ouverts est une carte, pas une note de travail : elle dit par où entrer et ce qui n'est pas surveillé. Le reste du dépôt peut rester public sans dommage ; publier qu'on utilise Tailscale et une YubiKey n'affaiblit rien, la sécurité ne tenant pas au secret du dispositif.


SSH

~/.ssh/config est versionné ; les clés ne le sont jamais. github.com n'accepte plus que les YubiKeys — une par token, -usbc au quotidien, -usba au coffre. Pousser exige une présence physique, la clé privée ne sortant jamais de l'applet FIDO.

L'épinglage dans config ne suffisait pas : c'est une préférence locale, et GitHub continuait d'accepter la clé logicielle. Quiconque volait le fichier écrivait sa propre config et poussait. La clé logicielle a donc été retirée du compte — c'est ce retrait, pas le fichier de config, qui rend la règle réelle. Elle reste en service pour tous les autres hôtes.

Les clés sont résidentes : le token est sa propre sauvegarde. Sur une machine neuve, ssh-keygen -K les régénère depuis la YubiKey — rien à mettre dans secrets/, et pas d'amorce circulaire (pas de clé SSH enfermée dans le dépôt qu'il faudrait SSH pour cloner). Leurs noms de fichier sont laids exprès : ce sont ceux que -K recrée seul à partir de la chaîne application.

$ git push          # sans le token
sign_and_send_pubkey: signing failed for ED25519-SK — agent refused operation
git@github.com: Permission denied (publickey).

C'est le comportement voulu, pas une panne. Le multiplexage garde la connexion 10 min : un seul touch pour une rafale de commandes.

Le même message veut dire deux choses. agent refused operation sort aussi bien quand aucun token n'est branché que quand le bon token est branché mais n'a pas été touché dans la fenêtre. Le seul moyen de les distinguer est la durée : un refus immédiat, c'est l'absence de token ; un refus après ~20 s par clé, c'est un touch manqué. Avec deux IdentityFile, ssh les essaie l'une après l'autre — d'où ~40 s avant le Permission denied final.

time git push    # ~0 s : pas de token. ~40 s : personne n'a touché.

Corollaire pratique : ce n'est pas automatisable. Une commande lancée par un agent, un cron ou un script attend un doigt qui n'arrive jamais et échoue au bout de 40 s. Le push est un geste manuel, par construction.

Enfin, hors terminal, ssh ne demande pas la passphrase lui-même : il délègue à un askpass externe, et sans askpass il abandonne sans rien afficher. install.sh installe donc ssh-askpass-gnome (Pop!_OS) ou ksshaskpass (Arch), et init.bash pose SSH_ASKPASS sur le premier des deux qui existe. Tant que gnome-keyring détient les passphrases le manque ne se voit pas — il se voit le jour où un agent propre doit charger une clé sans tty.

Le fichier a un piège documenté sur place — IdentityFile est la seule option d'un ssh_config qui soit cumulative au lieu de premier-gagnant. Mise dans Host *, la clé logicielle s'ajouterait à celle de github.com et ssh retomberait dessus dès le token absent, ce qui annulerait l'épinglage. D'où le bloc négatif Host * !github.com. Le contrôle tient en une commande :

ssh -G github.com | grep '^identityfile' | grep -c heartbeat   # doit valoir 0

Le contrôle porte sur la nature des clés, pas sur leur nombre : github.com en liste deux, une par YubiKey. Ce qui doit être absent, c'est la clé logicielle.

install.sh crée ~/.ssh en vrai dossier avant de stower. Sans ça, stow le plierait en un lien vers le dépôt et le premier ssh-keygen écrirait une clé privée dans un dépôt public. .gitignore couvre le cas en second rideau : common/.ssh/* est refusé par défaut, seul config est suivi.


Signature des commits

Chaque commit est signé par SSH, avec la même YubiKey qui authentifie. Un commit à ton nom exige donc une présence physique : une machine compromise ne peut pas en fabriquer.

Par SSH et non par GPG, pour deux raisons. Aucune clé à faire expirer — la GPG de ce dépôt l'a fait, sans prévenir. Et la clé matérielle est déjà là : une seule chose à protéger plutôt que deux.

user.signingkey vit dans identity.gitconfig, chiffré, et pas dans le .gitconfig committé : c'est un choix de poste, chaque machine nommant le token qu'elle a sous la main.

allowed_signers est ce qui permet de vérifier et pas seulement de signer — sans lui git n'a aucun moyen de relier une adresse à une clé publique :

git log --show-signature -1
git verify-commit HEAD

Le namespaces="git" y cantonne la confiance à la signature de commits. La même clé sert à s'authentifier auprès de GitHub, et une clé valable partout est une clé qu'on ne peut pas révoquer pour un seul usage.

Le coût est un touch par commit, y compris à chaque commit rejoué par un rebase. Pour une opération en lot :

git -c commit.gpgsign=false rebase ...

Si la clé du quotidien est perdue

Git ne prend qu'une clé de signature, sans repli — contrairement à l'authentification SSH qui essaie une liste. La YubiKey de secours est déjà enregistrée chez GitHub en type signing, donc la bascule tient en une ligne :

# dans ~/.config/git/identity.gitconfig
signingkey = ~/.ssh/id_ed25519_sk_rk_github-usba.pub

dots-secrets seal identity.gitconfig   # aucun token requis

Rien à faire côté GitHub, aucun commit à re-signer. C'est tout l'intérêt d'avoir enregistré les deux clés pendant que tout allait bien : le jour de la panne, il ne reste qu'un fichier à éditer, pas un compte à manipuler.


Mises à jour

Sur Pop!_OS, les correctifs de sécurité s'installent seuls. Les timers apt-daily et apt-daily-upgrade tournaient déjà — le paquet apt les arme d'office — mais sans unattended-upgrades ils n'avaient rien à exécuter, et sans APT::Periodic dans 20auto-upgrades le paquet lui-même ne fait rien. install.sh pose les deux.

Restent volontairement désactivés : le redémarrage automatique, qui ferait perdre du travail sur un poste de bureau (les correctifs de noyau attendent donc un reboot manuel), et tout ce qui dépasse les dépôts de sécurité — un poste ne doit pas changer de version majeure pendant la nuit.

Côté Omarchy, rien n'est automatisé, et c'est délibéré. Arch n'a pas de dépôt de sécurité séparé : automatiser reviendrait à lancer un pacman -Syu complet sans surveillance, avec son risque de mise à jour partielle et ses interventions manuelles annoncées sur la page d'accueil. Sur Arch, mettre à jour reste un geste conscient.


Détection de fuites

.gitignore, le filtre autoMode et le hook pre-commit ne couvrent que les fuites anticipées : un chemin qu'on a pensé à interdire. gitleaks regarde le contenu, pas le nom du fichier. C'est la différence entre une liste d'interdits et une relecture.

Le scan tourne au pre-commit, sur l'index, et refuse le commit en cas de trouvaille — comme il refuse de s'exécuter si gitleaks est absent. Laisser passer dans ce cas donnerait précisément l'illusion d'être couvert.

install.sh prend le binaire amont sur Pop!_OS, pas le paquet apt : Ubuntu livre 8.16 (mi-2023), dont le jeu de règles ignore par exemple les clés Anthropic — vérifié. Pour un scanner, la fraîcheur des règles est la fonction. L'archive est vérifiée par empreinte SHA256 avant installation : un outil censé ne pas faire confiance au reste ne peut pas se fier au transfert. Côté Arch le dépôt suit l'amont, le paquet suffit.

.gitleaks.toml n'écarte que des faux positifs structurels : les .age (du chiffré, haute entropie par construction — les signaler reviendrait à signaler que le coffre est bien fermé), les clés publiques age et SSH, les .sample, et les plugins tmux vendorisés. Rien n'y nomme d'employeur ni de point d'accès : le fichier est committé, une règle taillée sur un secret précis le décrirait.

Un secret committé ne se supprime pas, il se révoque. Réécrire l'historique ne rattrape ni les clones, ni les forks, ni les caches. C'est pour ça que le filet est au pre-commit et pas ailleurs : après, il est trop tard, et le seul recours est la rotation de la clé.

gitleaks git --redact -c .gitleaks.toml .    # tout l'historique
gitleaks dir --redact -c .gitleaks.toml .    # l'arbre courant

Accès distant

Tout le reste de ce dépôt organise des flux sortants. Ici on ouvre une porte, et la forme de la porte compte plus que ses réglages.

Il n'y a pas d'openssh-server sur ces machines, et rien n'écoute sur le LAN ni sur Internet. L'accès passe par le SSH intégré à Tailscale, qui authentifie par l'identité du pair dans le réseau maillé — pas par une clé posée sur le client. C'est la seule raison de ce choix : depuis Android, une YubiKey est inutilisable en SSH (l'OpenSSH de Termux est compilé sans FIDO, et l'accès USB-HID ou NFC au token n'est pas ouvert aux applis non root). Un authorized_keys classique aurait donc signifié déposer sur le téléphone exactement la clé logicielle qu'on vient de retirer de GitHub. Avec Tailscale SSH, il n'y a pas de clé sur le téléphone du tout : l'identité est celle de l'appareil dans le réseau, adossée à ton fournisseur d'identité — que tu peux protéger par la YubiKey en passkey. La chaîne matérielle est rétablie par un autre chemin.

install.sh installe Tailscale et active le démon, mais ne lance jamais tailscale up : rattacher une machine à un réseau est une décision, pas une étape d'installation. À faire à la main, une fois par machine :

sudo tailscale up --ssh          # rattache la machine ET active le SSH Tailscale
tailscale status                 # vérifier qui est joignable

Côté téléphone : l'application Tailscale, plus n'importe quel client SSH (Termux). Aucune clé à générer.

Dans les ACL du réseau, restreindre l'usage à tes propres appareils et exiger une ré-authentification périodique plutôt qu'un accès permanent :

"ssh": [
  {
    "action": "check",           // "accept" = permanent ; "check" = ré-auth
    "checkPeriod": "12h",        // au-delà, le navigateur redemande l'identité
    "src":  ["autogroup:member"],
    "dst":  ["autogroup:self"],
    "users": ["autogroup:nonroot"]
  }
]

check est le point important : sans lui, un téléphone volé et déverrouillé donne un shell permanent. Avec, il redemande l'identité — donc la passkey.

Pour piloter une session de travail depuis le téléphone, se rattacher à un tmux existant plutôt qu'ouvrir un shell neuf :

ssh <machine> -t 'tmux new -A -s phone'

new -A rattache la session si elle existe, la crée sinon : la même session survit aux coupures réseau, qui sont la règle en mobilité.

Le poste fixe ne dort plus

Une machine endormie n'a pas de tailscaled éveillé : elle est injoignable, et aucun réglage sur la machine n'y change rien. Le Wake-on-LAN se fait par un paquet envoyé sur le réseau LOCAL, ce qui suppose un appareil déjà allumé pour l'émettre — un Raspberry Pi, un NAS, une box qui sache le faire. Il n'y en a pas ici.

Le choix est donc assumé : install.sh désactive la suspension automatique sur le poste fixe, en écrivant None dans le réglage COSMIC CosmicIdle/v1/suspend_on_ac_time. L'écran continue de s'éteindre au bout de 30 min — c'est l'essentiel de l'économie, et ça ne coupe pas le réseau. Seule la suspension disparaît.

C'est le seul endroit du dépôt où l'accès distant coûte quelque chose : la consommation d'un fixe qui ne s'endort plus. Le contraire aurait coûté un accès qui marche une fois sur deux, ce qui ne vaut rien.

Le réglage ne s'applique qu'à un châssis de bureau (types DMI 3/4/6/7) : sur un portable la suspension est un service rendu, et le profil popos pourrait un jour tourner ailleurs que sur cette tour.

Vérifier que ça tient — l'écriture du fichier ne prouve rien, seule l'absence de suspension le prouve :

cat ~/.config/cosmic/com.system76.CosmicIdle/v1/suspend_on_ac_time   # -> None
journalctl --since "7 days ago" | grep -c "PM: suspend entry"        # -> 0

Avant ce changement, cette machine s'endormait 14 fois par mois.


Caméra

Une StreamCam est branchée sur le poste fixe. dots-cam permet de l'allumer depuis le téléphone — à travers le réseau maillé, jamais autrement.

dots-cam snap [fichier]     # une image, la caméra se referme aussitôt
dots-cam start [secondes]   # flux MJPEG, 30 min par défaut
dots-cam stop
dots-cam status

Le flux s'ouvre sur http://<adresse-tailscale>:8080/, lisible dans le navigateur du téléphone sans rien installer.

Trois propriétés, et ce sont elles qui ont dicté le code :

Éteinte par défaut. Rien n'est enable, rien ne démarre au boot. Le flux est une unité systemd transitoire : elle n'existe pas tant qu'on ne l'a pas demandée et disparaît en s'arrêtant. Il n'y a pas d'état oublié quelque part.

Jamais hors du réseau maillé. Le flux se lie à l'adresse Tailscale, et le script refuse de démarrer si elle n'existe pas. Se lier à 0.0.0.0 « en attendant » offrirait la caméra au réseau local entier — exactement le repli commode que ce dépôt passe son temps à retirer.

Extinction automatique. RuntimeMaxSec coupe au bout du délai, que la connexion ait tenu ou non. Le risque n'est pas d'allumer une caméra, c'est de la laisser allumée.

S'y ajoute un témoin que le logiciel ne contrôle pas : la LED blanche de la StreamCam est allumée tant que le capteur diffuse. Aucune commande de ce dépôt ne l'éteint. Chaque allumage laisse par ailleurs une trace dans le journal systemd, l'unité y étant nommée.

install.sh ajoute l'utilisateur au groupe video, et ça n'est pas du confort : sans lui, l'accès à /dev/video0 ne tient qu'à l'ACL que logind pose pour la session graphique active. La caméra marcherait tant qu'on est assis devant, et échouerait précisément quand on est loin — le seul moment où elle sert. Une reconnexion est nécessaire pour que le groupe prenne effet.

Le mode snap est le moins exposé des deux : la caméra s'ouvre le temps d'une image et se referme, rien n'écoute sur le réseau. Si le flux tourne déjà, la capture lui est demandée à lui plutôt que d'échouer sur un « device busy » incompréhensible.

Côté Omarchy, rien : la caméra est sur le fixe, et ustreamer n'est pas dans les dépôts officiels d'Arch.


Machine-specific

Le clair de ces fichiers n'est jamais committé. Certains sont restaurables depuis leur version chiffrée, les autres sont propres à la machine et doivent être réécrits à la main :

Fichier Usage Après un reset
~/.config/bash/local.bash secrets, clés API, overrides — sourcé en dernier déchiffré
~/.config/bash/work.bash aliases pro : noms de projets internes .sample à recopier
~/.config/git/identity.gitconfig nom et email git déchiffré
~/.config/zed/mcp.json serveurs MCP de Zed (endpoints internes) .sample à recopier
~/.local/bin/dev-tmux lanceur de session tmux propre au boulot à réécrire
~/.config/ghostty/local.conf font-size selon le DPI ; posé par le profil popos pour l'opacité à refaire (dépend de l'écran)
~/.config/git/work.gitconfig identité pro, appliquée d'office sous ~/Work/ déchiffré
omarchy/.config/hypr/monitors.conf résolutions et scaling (committé, mais par profil) committé
common/.claude/settings.private.json bloc autoMode de Claude Code, extrait par le filtre git régénéré par le filtre

Chacun a son .sample committé à côté, qui montre la structure sans nommer l'employeur, les projets internes ni l'identité. work.bash n'est chargé que s'il existe — une machine fraîche ne casse pas.

Sur une seconde machine

Aucune clé ne se copie d'une machine à l'autre — ni par clé USB, ni par le dépôt. Les sk sont des handles : la clé privée ne quitte jamais l'applet FIDO, et le token la redonne à la demande. Après le clone :

./install.sh                 # ~/.ssh en vrai dossier + sockets, pcscd, askpass
dots-secrets unseal          # YubiKey PIV : identity.gitconfig, local.bash…
cd ~/.ssh && ssh-keygen -K   # YubiKey FIDO : régénère la sk du token branché
chmod 600 ~/.ssh/id_ed25519_sk_rk_github-*

Sur Arch, ssh-keygen -K échoue tant que libfido2 manque : Debian en fait une dépendance dure d'openssh-client, Arch une simple optdepend. Le message ne parle ni de FIDO ni de YubiKey (libfido2.so.1: cannot open shared object file, puis unexpected internal error) et arrive après le PIN, ce qui laisse croire à un problème de token. install.sh l'installe.

ssh-keygen -K écrit dans le répertoire courant — d'où le cd, sans quoi les fichiers atterrissent dans le dépôt. Il ne rend que les identifiants du token branché : une seule YubiKey suffit, l'autre se régénère le jour où on la branche. Les noms recréés sont ceux qu'attend déjà ~/.ssh/config, il n'y a donc rien à renommer ni à éditer.

Ce qui reste propre à la machine : ~/.ssh/id_ed25519_heartbeat (clé logicielle pour les autres hôtes) et les fichiers marqués « à réécrire » ou « .sample à recopier » ci-dessus.


Claude Code

~/.claude/ est stowé depuis common/.claude/ : settings.json, les skills perso et la statusline suivent d'une machine à l'autre.

⚠️ Le chemin qui compte est ~/.claude/, pas ~/.config/claude/ — Claude Code ne lit jamais ce dernier. Il n'existe pas non plus de ~/.claude/settings.local.json : le suffixe .local n'a de sens qu'au niveau d'un projet.

~/.claude/settings.json est un lien vers le dépôt, et c'est volontaire : Claude Code écrit lui-même dans ce fichier (/config, thème, effort, plugins activés), donc ses réglages atterrissent directement dans le dépôt, prêts à committer. S'il venait à remplacer le lien par un vrai fichier — écriture atomique, comme cosmic-settings —, install.sh récupère le contenu et re-soude le lien.

Ne sont pas suivis : les serveurs MCP (ils vivent dans ~/.claude.json, mélangés à l'OAuth et aux caches, chaînes de connexion en clair), l'historique (projects/, 169 Mo), les plugins (ils se réinstallent seuls depuis enabledPlugins). Sur une machine neuve, les MCP sont à rejouer à la main :

claude mcp add anytype ...
claude mcp add mongodb ...

Le bloc autoMode et le filtre git

Ce dépôt est public, et settings.json porte un bloc autoMode.environment qui décrit l'environnement de travail (dépôt privé, bases Notion, politique de traitement des données personnelles). Un filtre git le retire :

.gitattributes associe le fichier au filtre claude-settings
.githooks/claude-settings-filter clean retire le bloc et l'archive ; smudge le réinjecte
common/.claude/settings.private.json l'archive, gitignorée
.githooks/pre-commit refuse le commit si le bloc atteint quand même l'index

Les deux sens comptent. Sans smudge, le filtre serait destructeur : un git stash, un git checkout ou un reset --hard fait transiter le fichier par l'index et le bloc disparaîtrait de la config vivante, sans un mot.

install.sh configure le tout (filter.claude-settings.*, core.hooksPath), avec required = true — un jq absent fait échouer l'indexation au lieu de publier le fichier brut. Un clone sur lequel install.sh n'a pas tourné n'a pas cette protection : le hook pre-commit est là pour ce cas.

Le panneau

Le panneau n'existe que dans les fenêtres tmux qui font tourner Claude Code. Il s'ouvre et se ferme tout seul : les hooks SessionStart et SessionEnd appellent tmux-claude-panel, qui réconcilie toutes les fenêtres avec les sessions vivantes. Une fenêtre docker ou npm n'en verra jamais.

La détection ne devine rien : chaque session Claude écrit son emplacement dans ~/.claude/sessions/<pid>.json, au format session:@fenêtre.%pane. Le script lit ce champ et vérifie que le pid vit encore.

prefix + a reste la commande manuelle, et cycle sur trois états :

fermé  ──▶  large (44 col)  ──▶  replié (5 col)  ──▶  fermé

Dans une fenêtre sans session Claude, il refuse et le dit. Fermer à la main pose une option de fenêtre @claude_panel_off : sans elle, le premier hook venu rouvrirait le panneau et « fermer » ne voudrait rien dire. Le drapeau vit le temps de la fenêtre ; prefix + a le lève.

Deux détails qui évitent des faux positifs : à SessionStart le fichier de session n'est pas forcément encore écrit, donc le hook fait compter d'office son propre pane ; à SessionEnd le processus est encore vivant, donc le hook exclut la session qui se termine — sinon elle se compterait elle-même et le panneau resterait. Comme SessionEnd part aussi pour les sous-agents, l'exclusion porte sur le session_id et non sur le pane : exclure un sous-agent ne retire rien, la session parente tient le panneau ouvert.

 CLAUDE                               13:49      ∘
                                                 ⠹
 AGENTS ────────────────────────────────  4      ∙
                                                 ∙
 ▸ ∘ migration-schema-v2       12j  bloqué       4
      attend un go avant de committer…
   ⠹ dotfiles-setup                48%  2m
   ∙ mon-autre-projet-b2           31%  3h      5h
                                               27%
 QUOTA ────────────────────────────────────     7j
                                               17%
   5 h   ████░░░░░░░░░░░░    27%   ↻ 1h20
   7 j   ███░░░░░░░░░░░░░    17%   ↻ 3j04h

 CONSO ────────────────────────────────────

   aujourd'hui   ~15.40 $   84k sortie
   7 j · ici    ~112.90 $  1.1M sortie

   ≈ équivalent API · 0.22 $/tour
   63% lecture · 22% cache 1h · 13% sortie

   api-platform    45%  █████░░░░░░░
   mon-projet      23%  ███░░░░░░░░░
   autre-projet    21%  ███░░░░░░░░░

   opus-5          96%  ████████████
   fable-5          2%  █░░░░░░░░░░░

Les titres de section sont prolongés par un filet jusqu'au bord, dans le gris de bordure du thème : ça sépare franchement, et le compteur reste calé à droite. Une ligne vide suit chaque titre — elle fait partie de l'en-tête, aucun appelant ne peut l'oublier. Le filet s'arrête une colonne avant le bord — écrire jusqu'à la dernière provoque un retour à la ligne différé sur certains terminaux, et une ligne vide parasite.

Quand le pane a le focus, la liste est navigable :

j / k, / , molette déplacer la sélection ()
Entrée sauter sur l'agent
clic sélectionner ; re-cliquer la ligne déjà sélectionnée saute
Suppr arrêter l'agent — deux fois, voir plus bas
q fermer le panneau

Sauter essaie trois pistes, dans l'ordre :

  1. l'emplacement que la session a enregistré (select-window + select-pane) ;
  2. à défaut, le pid remonté dans l'arbre des processus jusqu'à celui d'un pane — le champ tmux manque pour un agent lancé par claude attach, et le pid du pane est celui du shell, pas de claude ;
  3. claude attach <id> dans une fenêtre neuve, uniquement si l'agent ne tourne pas déjà.

Sinon il le dit et ne fait rien. Cette dernière garde n'est pas théorique : sessions/ et jobs/ se recouvrent dès qu'on attache un agent background — il gagne un fichier de session tout en gardant son état de job. Sans déduplication par sessionId il apparaît deux fois, et la ligne background, dépourvue d'emplacement tmux, relance un claude attach à chaque Entrée : d'où l'impression que sélectionner un agent le duplique. Les deux entrées sont donc fusionnées, la session vivante l'emportant tout en gardant l'id court pour claude attach/stop.

La sélection est mémorisée par sessionId et non par position, sinon elle sauterait d'un agent à l'autre au moindre changement d'ordre.

Un agent qui travaille tourne — ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ en accent — et sa colonne de droite bascule sur la durée du tour en cours au lieu de l'âge de la session : d'un agent qui tourne, ce qu'on veut savoir c'est depuis combien de temps il mouline, pas quand il a démarré.

L'animation est découplée du rafraîchissement des données. Les données ne sont relues qu'une fois par intervalle ; entre-temps le panneau ne réécrit que les caractères d'icône, par positionnement du curseur, à 120 ms. Couplées, les deux donnaient une icône qui change toutes les deux secondes — ça se lit comme un battement, pas comme un mouvement. Le braille s'impose pour la même raison : ✳ ✽ ✻ n'ont pas la même graisse, l'icône grossissait et rétrécissait.

Coût mesuré : 8 réveils par seconde et 0,2 % d'un cœur pendant qu'un agent travaille. Sans agent en cours il n'y a aucune image à peindre, et le panneau dort d'une traite.

Le contexte de chaque agent est en colonne de droite, à côté de la durée. C'est le chiffre qui mérite d'être scannable : une session à 85 % va compacter et perdre son historique sans prévenir. Il vire au jaune puis au rouge.

La durée n'est jamais l'âge de la session mais le temps écoulé depuis le dernier changement d'état : « travaille depuis 2m » et « inactif depuis 3h » se décident, « démarré il y a 4h » n'apprend rien.

L'agent de la fenêtre courante — et celui que tu pointes — portent l'accent et gagnent deux lignes de détail :

   ⠹ dotfiles-setup                48%  2m
     Opus 5 · high · ~46.89 $
     feat/claude-code · #115

Deux lignes courtes plutôt qu'une longue tronquée : une branche coupée en plein milieu devient trompeuse. La branche vient d'un git par dossier, mémorisé 30 s et appelé seulement pour les agents détaillés — un ou deux par rafraîchissement au pire. Le numéro de PR vient de pr.number, que seule la statusline reçoit.

La statusline n'écrit que lorsqu'une session rafraîchit sa ligne : une session endormie depuis une heure a des chiffres figés. Ils sont alors marqués périmé et grisés, jamais présentés comme frais.

Deux signaux sur deux éléments distincts : l'agent d'ici porte l'accent sur son nom, celui qui travaille anime son icône. Les cumuler sur le même élément les rendrait indistinguables, et l'icône garde sinon la couleur de son état — la mise en valeur ne doit pas manger l'information. Modèle, effort, contexte et coût viennent du cache sessions/<id>.json écrit par la statusline : le coût et le pourcentage de contexte ne sont calculés que côté client, aucun fichier de session ne les porte. Le panneau se sait « ici » en comparant sa propre fenêtre (TMUX_PANE) à celle de chaque agent.

Si Claude meurt sans passer par la sortie normale — kill -9, crash, pane fermé —, aucun SessionEnd ne part : le panneau le constate lui-même et se referme au bout de trois tours. Trois, pas un, pour ne pas disparaître sur un hoquet de lecture.

q pose le même drapeau @claude_panel_off qu'une fermeture par prefix + a : fermer, c'est fermer, quel que soit le chemin emprunté.

Arrêter un agent demande deux Suppr : le premier arme et colore la ligne en rouge, le second confirme. L'armement expire au bout de 5 s, et tout déplacement le lève — la confirmation ne vaut que pour la ligne visée, jamais pour celle où l'on vient d'arriver. Un délai minimum de 0,3 s sépare les deux : sans lui, une touche maintenue enfoncée armerait et confirmerait dans la même rafale de répétition automatique.

Deux chemins d'arrêt selon la nature de l'agent. Un agent background détaché passe par claude stop, qui conserve la conversation — elle reste reprenable par claude attach. Une session vivante reçoit un SIGTERM : elle se ferme proprement et ses hooks SessionEnd partent, donc le panneau de sa fenêtre se referme tout seul.

Le repli redimensionne au lieu de tuer : le processus survit, donc pas de rescan, et le rendu bascule tout seul en compact sous 14 colonnes. Un SIGWINCH réveille le panneau, il n'attend pas le tick suivant. Ni noms ni coûts en replié — à cinq colonnes, tout ce qui est tronqué est du bruit.

Les agents sont lus directement dans ~/.claude/{sessions,jobs}/. claude agents --json est l'interface documentée, mais elle coûte ~270 ms par appel (démarrage d'un runtime node) pour des données déjà sur le disque — 27 ms contre 290 ms par rendu, dans un pane qui se rafraîchit toutes les cinq secondes. Les fichiers donnent en prime le needs de chaque agent bloqué, affiché sous son nom : « bloqué » tout seul ne dit pas ce qu'on attend de toi. Si l'arborescence n'est pas celle attendue, le panneau repasse par la CLI plutôt que d'afficher un vide mensonger (--cli force ce chemin).

Les usages sont agrégés depuis les transcripts, en incrémental — un curseur d'octets par fichier dans ~/.cache/claude-panel/, sans quoi 169 Mo de JSONL seraient relus à chaque rafraîchissement. Les seaux sont heure × projet × modèle, et le modèle vient de chaque message, pas de la session : changer de modèle en cours de route est attribué correctement, à la réponse près.

Le chiffre mis en avant est le coût, pas le volume, et le volume affiché est celui des tokens de sortie. La raison tient dans une mesure : 98 % des tokens sont de la lecture de cache, qui ne pèse que 63 % de la note. Un compteur de tokens brut suit les relectures de contexte, pas le travail produit — et classer les projets par volume désignerait le mauvais coupable. Les trois ventilations (poste de coût, projet, modèle) sont donc en coût, sur 7 jours, une seule journée étant trop bruitée pour qu'un classement veuille dire quelque chose.

Le nom lisible d'un projet vient du cwd des entrées : le dossier de transcript est un chemin encodé dont on ne peut pas redéduire le nom, les tirets du chemin et ceux des dossiers s'y confondent.

Deux mentions ne sont pas décoratives. · ici : les transcripts d'une autre machine et les sessions web ne laissent aucune trace locale, donc la journée est complète mais la semaine ne l'est pas — les comparer sans le savoir induit en erreur. ≈ équivalent API : sur abonnement ces dollars ne sont pas la facture. 294 $ d'équivalent tiennent dans 21 % du quota hebdomadaire ; le chiffre sert à comparer un projet ou un modèle à un autre, le bloc QUOTA seul dit ce qui contraint. La table de tarifs est codée en dur, un modèle inconnu compte ses tokens sans être chiffré.

Le coût par tour est le chiffre le plus actionnable : à chaque échange, une session relit tout son contexte. Une session à 86 % d'une fenêtre de 1M coûte 0,43 $ le tour avant d'avoir généré un mot, contre 0,01 $ pour Haiku à 72 % de 200k — trente fois moins.

Conseils

Une section apparaît quand il y a quelque chose à faire, et disparaît sinon — un bandeau « tout va bien » permanent occupe la place de ce qui compte.

Déclencheur Conseil
contexte ≥ 60 % et tour ≥ 0,05 $ (ou contexte ≥ 90 %) /compact, avec le coût par tour évité
quota 5 h ≥ 75 % passer sur un modèle plus léger, avec l'heure de reset
quota 7 j ≥ 85 % lever le pied jusqu'au reset

Le double seuil sur le contexte n'est pas un raffinement gratuit : 72 % d'une fenêtre de 200k sur Haiku, c'est un centime par tour. Conseiller de compacter pour économiser un centime, c'est du bruit — et le bruit finit par faire ignorer la section entière.

Le quota et le coût ne s'inventent pas : rate_limits, cost.total_cost_usd et context_window.used_percentage ne sont exposés qu'au stdin de la statusline. common/.claude/statusline.sh les affiche et les dépose dans deux caches que le panneau relit — rate-limits.json pour le compte, sessions/<id>.json par session. Le panneau marque le quota « périmé » au-delà de 10 minutes plutôt que d'afficher une valeur morte.

Contrepartie : avec une statusline active, Claude Code masque la plupart des rappels de raccourcis du footer (dont esc to interrupt). Retirer la clé statusLine de settings.json les fait revenir.


Notes

  • Omarchy gère le système de base. Ces dotfiles sont des overrides — ~/.local/share/omarchy/ n'est jamais touché.
  • Hyprland recharge à chaud. Waybar demande omarchy restart waybar.
  • tpm vit dans ~/.config/tmux/plugins (XDG), pas ~/.tmux.

About

My personal config

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages