Je vais commencer par le scénario qui m'a fait construire RTK. Un vendredi après-midi, je laisse un agent travailler sur une branche de bugfix pendant que je rédige une spéc. L'agent se retrouve dans un état de conflit de merge — la situation est récupérable, mais l'agent en décide autrement. Il exécute git reset --hard origin/main. En trois secondes, il efface quatre heures de travail sur un worktree que j'avais mal isolé.
La commande n'était pas stupide de son point de vue : elle résolvait le conflit. Le problème, c'est qu'elle résolvait le conflit en jetant mon travail à la poubelle. L'agent n'avait aucun moyen de savoir que ce worktree contenait des modifications non commitées sur deux fichiers d'infrastructure que j'avais exclus du staging intentionnellement.
C'est l'histoire de RTK. Pas un framework, pas une plateforme — une fonction bash de 15 lignes et une discipline d'une ligne dans CLAUDE.md.
Le faux débat : restreindre vs autoriser
La réaction naturelle après ce genre d'incident est de restreindre. On liste les commandes autorisées, on bloque le reste, on dort mieux. Sauf que ça ne fonctionne pas.
Un agent dont on a retiré git, dotnet ef et rm ne peut plus travailler. Il ne peut pas committer ses propres corrections, il ne peut pas migrer la base locale, il ne peut pas nettoyer les artefacts de build. Vous récupérez un agent qui lit du code et vous explique ce qu'il faudrait faire — ce qui est utile, mais beaucoup moins que ce pour quoi vous l'avez configuré.
La vraie solution est architecturalement différente : un proxy. L'agent garde toutes ses permissions, mais chaque commande passe par un intermédiaire. Cet intermédiaire laisse passer par défaut, et n'intervient que lorsqu'une règle explicite correspond. Zéro friction sur les commandes sûres, guardrail précis sur les commandes dangereuses.
C'est exactement ce que fait RTK. Le nom vient de l'abréviation que j'utilise en interne — peu importe le nom, ce qui compte c'est le mécanisme.
L'implémentation : 15 lignes de bash
Voici le wrapper complet, tel qu'il tourne dans mon environnement depuis quatre mois :
#!/usr/bin/env bash
# rtk — proxy de commandes pour agents IA
# Usage : rtk <commande> [args...]
# ~/.local/bin/rtk (chmod +x)
RTK_LOG="${RTK_LOG:-$HOME/.rtk/audit.log}"
RTK_RULES="${RTK_RULES:-$HOME/.rtk/rules.json}"
RTK_DRY_RUN="${RTK_DRY_RUN:-0}"
mkdir -p "$(dirname "$RTK_LOG")"
_rtk_log() {
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | user=$(whoami) | exit=$1 | cmd=$2" >> "$RTK_LOG"
}
_rtk_check_rules() {
local full_cmd="$*"
if [[ ! -f "$RTK_RULES" ]]; then
return 0 # pas de fichier de règles → on laisse passer
fi
# Lecture des règles via jq — retourne le code d'action (0=pass, 1=block, 2=confirm)
local action
action=$(jq -r --arg cmd "$full_cmd" '
.rules[] |
select($cmd | test(.pattern)) |
.action
' "$RTK_RULES" 2>/dev/null | head -1)
case "$action" in
"block")
local reason
reason=$(jq -r --arg cmd "$full_cmd" '
.rules[] | select($cmd | test(.pattern)) | .reason
' "$RTK_RULES" 2>/dev/null | head -1)
echo "[RTK BLOCKED] $full_cmd" >&2
echo " Raison : $reason" >&2
_rtk_log "BLOCKED" "$full_cmd"
return 1
;;
"confirm")
local reason
reason=$(jq -r --arg cmd "$full_cmd" '
.rules[] | select($cmd | test(.pattern)) | .reason
' "$RTK_RULES" 2>/dev/null | head -1)
echo "[RTK CONFIRM] $full_cmd" >&2
echo " $reason" >&2
read -r -p " Confirmer ? [oui/non] " answer >&2
if [[ "$answer" != "oui" ]]; then
_rtk_log "DENIED" "$full_cmd"
return 1
fi
;;
"redirect")
local replacement
replacement=$(jq -r --arg cmd "$full_cmd" '
.rules[] | select($cmd | test(.pattern)) | .replacement
' "$RTK_RULES" 2>/dev/null | head -1)
echo "[RTK REDIRECT] $full_cmd → $replacement" >&2
_rtk_log "REDIRECT→$replacement" "$full_cmd"
eval "$replacement"
return $?
;;
esac
return 0
}
# Point d'entrée principal
full_cmd="$*"
_rtk_check_rules "$full_cmd" || exit $?
if [[ "$RTK_DRY_RUN" == "1" ]]; then
echo "[RTK DRY-RUN] Aurait exécuté : $full_cmd" >&2
_rtk_log "DRY-RUN" "$full_cmd"
exit 0
fi
_rtk_log "0" "$full_cmd"
exec "$@"
Le script fait une chose et la fait bien. Pour chaque commande : vérifier les règles → si dry-run, afficher sans exécuter → sinon logger et passer la main avec exec. L'exec est important : il remplace le processus bash par le processus fils, ce qui élimine tout overhead de forking sur les commandes normales.
La structure des règles
Les règles vivent dans ~/.rtk/rules.json. Chaque règle a un pattern (regex), une action, et une raison humaine pour l'audit :
{
"rules": [
{
"pattern": "git push.*--force(?!-with-lease)",
"action": "block",
"reason": "git push --force est interdit. Utilisez --force-with-lease pour éviter d'écraser des commits distants non lus."
},
{
"pattern": "git reset --hard",
"action": "confirm",
"reason": "git reset --hard va détruire les modifications non commitées. Vérifiez git worktree list et git status avant de confirmer."
},
{
"pattern": "dotnet ef database drop",
"action": "confirm",
"reason": "Suppression de base de données. Confirmez que vous êtes sur l'environnement cible correct (pas prod)."
},
{
"pattern": "rm -rf",
"action": "redirect",
"replacement": "trash",
"reason": "rm -rf est redirigé vers trash pour permettre la récupération."
},
{
"pattern": "git push.*origin.*main$",
"action": "confirm",
"reason": "Push direct sur main. Passez par une PR sauf si vous avez une raison explicite."
}
]
}
Les patterns sont des expressions régulières POSIX étendues, évaluées via jq avec test(). Le premier match gagne — l'ordre des règles est donc significatif. Notez le lookahead négatif sur la première règle : --force(?!-with-lease) bloque --force seul mais laisse passer --force-with-lease, qui est la forme sûre.
L'instruction CLAUDE.md : une ligne suffit
Pour que l'agent utilise RTK systématiquement, une seule ligne dans le CLAUDE.md global suffit :
# RTK
Always prefix shell commands with `rtk`. It passes through unchanged if no filter exists.
Even in command chains: `rtk git add . && rtk git commit -m "msg" && rtk git push`
Claude Code lit CLAUDE.md à chaque démarrage de session. L'instruction est simple, non-ambiguë, et s'applique même aux chaînes de commandes. Sur quatre mois d'utilisation en production, je n'ai eu aucun cas où l'agent a "oublié" le préfixe — la formulation "always prefix" est suffisamment directive.
Les 4 règles concrètes qui m'ont économisé du temps réel
1. git push --force → bloqué, remplacé par --force-with-lease
git push --force écrase silencieusement les commits que vos collègues ont pushé depuis votre dernier fetch. L'agent ne le sait pas — il a juste besoin de "mettre à jour la branche distante". La règle ne bloque pas le push, elle bloque la forme non-sûre. --force-with-lease vérifie que vous avez bien le dernier état distant avant d'écraser. L'agent peut toujours forcer un push — mais pas sans filet.
2. dotnet ef database drop → confirmation obligatoire
Cette commande supprime la base de données entière. En développement local c'est souvent ce qu'on veut. En CI, c'est une catastrophe. La règle de confirmation affiche la raison, attend "oui" explicite. L'agent ne peut pas confirmer de lui-même — la confirmation est interactive et dirige vers stderr, ce qui casse la lecture automatique de la sortie.
3. rm -rf → redirigé vers trash
La commande trash (trash-cli sur Linux, trash via Homebrew sur macOS) déplace les fichiers dans la corbeille système au lieu de les supprimer définitivement. L'agent fait son nettoyage, vous pouvez récupérer si nécessaire. La redirection est transparente — l'agent ne sait pas qu'il utilise trash plutôt que rm.
4. git reset --hard → confirmation avec contexte
C'est la règle qui m'aurait sauvé le vendredi en question. La confirmation affiche le message d'avertissement et attend. Mais surtout, le message invite explicitement à vérifier git worktree list et git status avant de confirmer. Même si l'utilisateur confirme à tort, il a eu l'information pour décider.
L'audit trail : votre journal d'activité agent
Chaque commande exécutée via RTK est loguée dans ~/.rtk/audit.log. Voici à quoi ressemble un extrait réel après une session de feature development :
2026-04-22T09:12:04Z | user=mikael | exit=0 | cmd=dotnet build Edenred.Smarter.Client.Bff.Api.slnx
2026-04-22T09:12:31Z | user=mikael | exit=0 | cmd=dotnet test tests/Edenred.Smarter.Client.Bff.Tests --no-build
2026-04-22T09:14:08Z | user=mikael | exit=0 | cmd=git add src/Edenred.Smarter.Client.Bff.Application/Commands/CreateOrderCommand.cs
2026-04-22T09:14:09Z | user=mikael | exit=0 | cmd=git commit -m "red: add CreateOrder command tests"
2026-04-22T09:22:47Z | user=mikael | exit=0 | cmd=git add src/
2026-04-22T09:22:48Z | user=mikael | exit=0 | cmd=git commit -m "green: implement CreateOrder handler"
2026-04-22T09:31:12Z | user=mikael | exit=BLOCKED | cmd=git push --force origin feat/LCT-42500
2026-04-22T09:31:14Z | user=mikael | exit=0 | cmd=git push --force-with-lease origin feat/LCT-42500
2026-04-22T09:44:03Z | user=mikael | exit=DENIED | cmd=dotnet ef database drop --project src/Infra
2026-04-22T09:44:19Z | user=mikael | exit=0 | cmd=dotnet ef database drop --project src/Infra
Ce log répond à une question que vous posez régulièrement après un incident : "qu'est-ce que l'agent a fait exactement ?". Avec un journal brut, vous pouvez rejouer la session, identifier le moment exact où quelque chose a divergé, et comprendre la séquence de décisions qui a mené à l'état problématique. Sans ce log, vous déboguez dans le noir.
Le log est également votre argument face à un collègue sceptique sur les agents IA. Ce n'est pas "l'agent a fait n'importe quoi" — c'est "voici exactement ce qu'il a fait, à quelle heure, avec quel résultat".
Le mode dry-run : tester une nouvelle skill sans risque
Quand j'écris une nouvelle skill agent — un nouvel ensemble d'instructions pour un workflow spécifique — je ne sais pas encore ce qu'elle va exécuter. Le mode dry-run résout ça :
RTK_DRY_RUN=1 claude -p "Read ~/.claude/skills/new-skill/SKILL.md and run the full workflow for: [story]"
L'agent tourne intégralement. Il planifie, raisonne, prépare ses commandes — mais RTK affiche [RTK DRY-RUN] Aurait exécuté : <commande> sans exécuter quoi que ce soit. En fin de session, je lis le log dry-run et je vois exactement ce que la skill aurait fait.
C'est la même idée que terraform plan avant terraform apply, ou dotnet ef migrations script avant d'appliquer une migration. Vous ne faites confiance à une nouvelle chose qu'après l'avoir observée fonctionner sans conséquences.
En pratique, le dry-run identifie deux types de problèmes : les commandes inattendues (l'agent fait quelque chose que vous n'aviez pas anticipé dans le workflow) et les commandes dans le mauvais ordre (un dotnet ef database drop avant le dotnet ef migrations add — logique inversée qui aurait détruit des données locales).
Au-delà de la protection : RTK pour l'instrumentation
Une fois que toutes les commandes passent par RTK, vous avez un point d'observation unique sur l'activité de développement. J'ai ajouté une variante de RTK qui mesure la durée des builds et l'écrit dans un fichier de métriques local :
# Variante pour rtk dotnet build — mesure la durée
if [[ "$1 $2" == "dotnet build" ]]; then
local start_ts
start_ts=$(date +%s%N)
exec "$@"
local duration=$(( ($(date +%s%N) - start_ts) / 1000000 ))
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | build_ms=$duration | cmd=$*" >> "$HOME/.rtk/metrics.log"
fi
Avec quelques semaines de données, vous pouvez répondre à une question que personne ne pose jamais mais qui est pourtant fondamentale : est-ce que l'utilisation d'agents IA augmente ou diminue le nombre de builds par feature ? Les builds sont-ils plus courts (l'agent écrit du code plus propre dès le premier essai) ou plus longs (l'agent itère plus souvent) ?
Sur mon contexte — .NET 9, architecture BFF + orchestrateur, pipeline TDD multi-agents — les builds par feature ont diminué de 40 % en comparant les six semaines avant et après la mise en place du pipeline. Ce chiffre est issu directement des métriques RTK. Sans ce log, je n'aurais eu qu'une impression.
Installation en trois minutes
Pour reproduire l'environnement RTK complet :
# 1. Créer le répertoire RTK
mkdir -p ~/.rtk ~/.local/bin
# 2. Installer le wrapper (copier le script ci-dessus dans ce fichier)
vim ~/.local/bin/rtk
chmod +x ~/.local/bin/rtk
# 3. Vérifier que ~/.local/bin est dans le PATH
echo $PATH | grep -q "$HOME/.local/bin" || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
# 4. Créer le fichier de règles initial
cp /dev/stdin ~/.rtk/rules.json <<'EOF'
{ "rules": [] }
EOF
# 5. Tester
rtk echo "RTK fonctionne"
# → RTK fonctionne (logué dans ~/.rtk/audit.log)
# 6. Ajouter l'instruction dans CLAUDE.md
# Always prefix shell commands with `rtk`. It passes through unchanged if no filter exists.
Installez jq si ce n'est pas déjà fait (brew install jq sur macOS, apt install jq sur Debian/Ubuntu). Et installez trash-cli si vous voulez la redirection rm -rf (pip install trash-cli ou npm install -g trash-cli).
Ce que RTK ne remplace pas
RTK ne remplace pas le jugement. Si vous donnez à un agent un CLAUDE.md qui dit "résous tous les conflits de merge en prenant toujours la version distante", aucun proxy ne vous sauvera — l'agent exécutera exactement ce que vous lui avez demandé, et RTK le loguera fidèlement.
RTK ne remplace pas non plus les tests et les revues de code. Ce n'est pas parce que git push --force est bloqué que le code pushé est correct. RTK protège l'infrastructure Git et la base de données — il ne protège pas la logique métier.
Enfin, RTK ne remplace pas la compréhension de ce que fait l'agent. Si vous ne lisez pas le log d'audit régulièrement, vous perdez la moitié de la valeur du système. Le log est un outil de compréhension autant que de sécurité.
"RTK n'est pas là pour compenser un agent mal configuré — il est là pour que vous puissiez faire confiance à un agent bien configuré. La nuance est essentielle."
La vraie proposition de valeur de RTK, c'est qu'elle déplace le curseur de "je ne fais pas confiance aux agents sur ma machine" vers "je fais confiance aux agents dans les limites que j'ai explicitement définies". Ce n'est pas de l'autonomie aveugle ni de la restriction paralysante — c'est de la supervision architecturée.
Et ça, ça se construit en 15 lignes de bash.
Vous voulez mettre en place un workflow agent IA sécurisé dans votre équipe .NET ?
Je peux auditer votre configuration actuelle, construire vos guardrails RTK,
ou intervenir en mission sur vos projets .NET 9.