Sécurité, souveraineté et limites¶
Traduction française de la version anglaise. En cas de différence, la version anglaise fait foi.
Ce document dit ce que tenon garantit, et surtout ce qu'il ne garantit pas. Il décrit le comportement du code actuel. Le mode d'emploi détaillé est dans utilisation.md.
Souveraineté : critères retenus¶
La souveraineté de tenon ne repose pas sur la nationalité des fournisseurs. Elle repose sur quatre critères vérifiables. Un service utilisé par défaut pour faire fonctionner, construire ou distribuer tenon doit les remplir tous :
- Ouvert : logiciel open source, ou protocole ouvert et documenté.
- Auto-hébergeable : vous pouvez le remplacer par votre propre instance.
- Droit applicable européen : l'opérateur n'est soumis à aucune loi extraterritoriale qui permettrait à une autorité non européenne d'accéder aux données (par exemple CLOUD Act, FISA section 702).
- Données maîtrisées : aucun transfert de données hors de l'UE sans votre consentement explicite, tracé dans le journal d'audit.
Un service qui ne remplit pas ces critères reste utilisable. Il n'est jamais utilisé par défaut, et son usage est toujours explicite et tracé.
Conséquences dans tenon :
- Les fournisseurs préconfigurés sont européens (
eu) ou locaux (local). Le fournisseur par défaut est Mistral AI. - Un fournisseur ou un serveur MCP
non-euexige votre accord : question dans l'interface interactive pour un fournisseur, option--allow-non-eusinon. - Les bibliothèques sont vendorées : la compilation depuis les sources ne demande aucun proxy de modules (
GOFLAGS=-mod=vendor).
Ce que tenon garantit¶
Journal d'audit¶
- Chaque événement (message, appel d'outil, permission, résultat, erreur) est écrit dans
~/.local/share/tenon/audit/AAAA-MM-JJ.jsonl, avecfsync. - Chaque ligne contient l'empreinte SHA-256 de la ligne précédente. La première ligne a
prev = "sha256:0". tenon audit verifydétecte une ligne modifiée, insérée ou supprimée au milieu du journal.- Le contenu est masqué avant écriture. Avec
audit.level = "metadata", seules l'empreinte et la taille de chaque charge utile sont gardées. - Si l'écriture échoue, tenon n'exécute plus aucune action : la conformité passe avant la continuité.
- Avec
[audit.timestamp], une autorité d'horodatage signe le hash de tête (voir Horodatage).
Permissions¶
- Chaque appel d'outil passe par le moteur de permissions avant exécution.
- Par défaut, seule la lecture dans le projet est autorisée sans demande. Lire
.env*,*.pemouid_rsa*, ou accéder hors du projet, demande toujours une validation. - Une règle
denyl'emporte toujours. Sur un chemin, elle s'applique aussi à la forme avant résolution des liens symboliques, sans tenir compte de la casse. - Un dépôt ne peut pas assouplir les règles : sa configuration ne peut contenir ni règle
allow, ni fournisseur, ni serveur MCP, et elle ne peut ni désactiver l'isolation ni accorder le réseau à une règle. - En mode non interactif, toute demande de validation devient un refus.
- Dans l'interface interactive, la validation exige un délai de calme de 500 ms, une commande entière visible et au moins 40 colonnes (voir utilisation.md).
Masquage¶
- Les secrets (clés d'API, jetons, clés privées, valeurs de type
.env) et des données personnelles (IBAN, NIR, e-mail, téléphone français) sont remplacés par des marqueurs :[SECRET_1],[EMAIL_1]… - Le masquage s'applique à tout ce qui part vers le fournisseur, au journal d'audit, aux sessions et aux sorties affichées.
- Les clés d'API des fournisseurs configurés et les valeurs
env_from/bearer_envdes serveurs MCP sont masquées à l'identique, même sans forme reconnaissable. - Les marqueurs ne sont jamais restaurés. Un appel MCP dont les arguments contiennent un marqueur est refusé.
Transparence¶
- Le modèle, le fournisseur, le pays et la juridiction sont affichés au démarrage, dans la barre d'état et dans l'événement
SessionStartedde la sortie JSON. - Les commits qui contiennent des fichiers écrits par l'agent reçoivent les trailers
AI-Assisted-ByetAI-Session(voir utilisation.md).
Export de conformité¶
tenon audit exportne fait que lire le journal : il ne le modifie ni ne le verrouille.- Le contenu est de nouveau masqué à la construction du rapport, et les caractères de contrôle sont remplacés par des espaces.
- Le rapport HTML ne contient aucun script et ne charge aucune ressource externe ; sa Content-Security-Policy est
default-src 'none'; style-src 'unsafe-inline'. - Les deux fichiers sont créés en mode
0600et ne remplacent jamais un fichier existant. - Sans
--details, les fichiers gardent des textes masqués tirés du journal : chemins des fichiers modifiés, identifiants de session, messages d'erreur, textes des pannes MCP et raisons des hooks. Manipulez aussi le rapport par défaut avec soin. - Avec
--details, les fichiers contiennent des données de conversation (prompts et commandes en entier ; réponses, arguments et résultats d'outils coupés à 500 caractères). Conservez-les et partagez-les comme le journal lui-même. Le HTML affiche un bandeau quand elles sont incluses. report_idethtml_sha256détectent une modification accidentelle ; ils n'arrêtent pas qui réécrit les deux fichiers. Voir utilisation.md.
Isolation système¶
- Les commandes shell et les serveurs MCP stdio sont confinés par le système : écritures limitées au projet, aux temporaires et aux caches ; liste intégrée de secrets illisibles ; réseau fermé (Landlock et espaces de noms sous Linux,
sandbox-execsous macOS). - Modes
auto(défaut),requiredetoff. Le mode effectif est le plus strict de la configuration globale, du projet, de la politique et du profil ; un projet ou une politique ne peut jamais produireoff.strict-localimposerequired. - Les variables de secrets (
api_key_envdes fournisseurs et de tous les préréglages,bearer_env,env_from) ne sont pas transmises aux commandes, dans tous les modes (sous macOS, une commande peut encore les lire dans un autre processus, voir plus bas). - La façon dont chaque commande s'est exécutée est dans le journal d'audit (
ToolResult.sandbox) et l'export de conformité. Voir L'isolation système et ses limites pour ce qui reste ouvert.
Routage par sensibilité des données¶
- Avec
[classification](chemins et marquages ; les marquages intégrés commeDIFFUSION RESTREINTEetEU RESTRICTEDsont toujours actifs) et[routing] local, une session qui touche du contenu classé devient sensible et n'est envoyée qu'au fournisseur local, compaction et sous-agents compris. - Sans fournisseur local, les appels d'outils sur des chemins classés sont refusés, les résultats d'outils marqués sont masqués, et un tour qui porte un marquage est bloqué (événement
Error,code: "classification"). - Avant chaque envoi à un fournisseur non local, tenon cherche des marquages dans la requête (contrôle sortant), avant et après masquage : une détection rend la session sensible et la réachemine, ou bloque le tour (le message suggère
/rewind). Le texte produit par un fournisseur non local lui-même (réponses, arguments d'appels d'outils, résumés) n'est pas recontrôlé : ce fournisseur l'a déjà. - La bascule faute de premier fragment (60 secondes) ne s'applique que si un autre candidat peut prendre le relais ; un fournisseur seul n'est jamais coupé, et toute activité du flux (fragments d'appels d'outils compris) arrête le délai.
- La bascule vers un autre fournisseur n'a lieu que pour une session normale, avant le premier fragment, et passe par les garde-fous de profil, de liste blanche, de politique et de consentement.
- Les décisions sont dans le journal d'audit (
SessionClassified,ProviderSelected) et l'export de conformité signale tout fournisseur non local atteint après le classement (code de sortie 2). Voir utilisation.md.
Profils de conformité¶
- Avec
profile = "eu-only","secnumcloud"ou"strict-local"(configuration globale ; un projet ne peut que le durcir), le fournisseur sélectionné et les serveurs MCP doivent respecter le profil, et--allow-non-euest refusé. - Le profil est contrôlé au démarrage puis avant chaque envoi au modèle : en cas de violation, rien n'est envoyé et le tour est bloqué (événement
Error,code: "profile"). - Le profil est enregistré dans
SessionStartedet listé dans l'export de conformité. - Sous
strict-local, les requêtes au fournisseur (détection du modèle local comprise) et aux serveurs MCPhttpignorent les variables d'environnement de proxy (HTTP_PROXY,HTTPS_PROXY) : une adresse privée acceptée par le profil ne passe jamais par un proxy. Soussecnumcloud, il en va de même pour un fournisseur local (détection du modèle local comprise) ; le gestionnaire MCP ignore les variables de proxy sousstrict-localetsecnumcloud. secnumcloudaccepte un destinataire local, ou un destinataireeudéclaré qualifiésecnumcloud. tenon ne vérifie jamais une qualification : la qualification SecNumCloud est une certification ANSSI d'un service, pas une propriété technique. tenon trace qui l'a déclarée (qualificationsdansSessionStarted.provideret dans l'export de conformité). Les préréglages n'en déclarent aucune.- Un profil borne les données envoyées au modèle et aux serveurs MCP. Il ne restreint pas ce que les commandes shell et les programmes MCP stdio envoient par le réseau (l'isolation le ferme par défaut, et
strict-localla rend obligatoire), et il ne change ni les permissions, ni le niveau d'audit, ni les hooks. Voir utilisation.md.
Liste blanche de fournisseurs¶
[provider_allowlist](hosts,local) dans la configuration globale, et éventuellement dans.tenon/config.toml, restreint les hôtes qu'un fournisseur peut utiliser, pour qui n'a pas de politique d'organisation. Toutes les listes présentes (politique, globale, projet) doivent accepter l'hôte du fournisseur : un projet ne peut que restreindre.- Elle est contrôlée au démarrage, avant tout contact, puis avant chaque envoi (événement
Error,code: "allowlist", tourblocked).--allow-non-eune la lève pas. - Message au démarrage :
fournisseur « <id> » (hôte « <h> ») absent de provider_allowlist (<globale|projet>)(code de sortie 1). - Elle ne s'applique pas aux serveurs MCP, et ne restreint ni les commandes shell ni les programmes MCP stdio (l'isolation ferme leur réseau par défaut). Voir utilisation.md.
Politique d'organisation¶
- Un administrateur peut déployer une politique signée (minisign) dans un dossier appartenant à root. tenon la vérifie à chaque démarrage et refuse de démarrer si un fichier manque, si la signature est mauvaise, si la politique est invalide, ou si le dossier ou ses fichiers n'appartiennent pas à root ou sont modifiables par le groupe ou les autres (échec fermé).
- La politique est un plancher : elle relève le profil, restreint les fournisseurs par hôte, ajoute des règles de permissions
deny/asket de masquage, impose le niveau d'audit et filtre les serveurs MCP. La configuration de l'utilisateur et du projet, ainsi que--allow-non-eu, ne peuvent pas l'assouplir. - Les hôtes des fournisseurs sont contrôlés au démarrage puis avant chaque envoi (événement
Error,code: "policy"). - La politique (id, version, identifiant de clé) est enregistrée dans
SessionStartedet listée dans l'export de conformité. - Les qualifications déclarées dans la configuration utilisateur (fournisseurs et serveurs MCP) sont ignorées sous une politique, avec un avis ; seules celles de
providers.definecomptent : un utilisateur ne peut pas s'auto-qualifier pour franchir un profilsecnumcloud. TENON_POLICY_DIRn'est lue que sans politique système : elle ne peut ni en retirer ni en remplacer une. Un utilisateur qui peut y écrire ne peut qu'ajouter une politique signée par une clé qu'il fournit, ce qui le restreint davantage.- Les règles de permissions s'appliquent aux commandes shell, mais la liste blanche d'hôtes des fournisseurs ne restreint pas les accès réseau faits par les commandes shell ou les programmes MCP stdio (la politique peut exiger l'isolation, qui le ferme par défaut), et un administrateur de la machine peut la retirer. Voir politique.md.
Limites connues¶
L'isolation système et ses limites¶
Les permissions décident si une commande s'exécute. L'isolation système (voir utilisation.md) limite ce qu'une commande shell ou un serveur MCP stdio peut faire une fois lancé. Elle est active par défaut (mode = "auto").
Modèle de menace. Sans isolation, une commande approuvée, ou couverte par une règle allow large (tool = "shell", match = "*"), écrit partout où vous le pouvez, lit vos secrets et les envoie par le réseau. L'isolation couvre trois choses : les écritures hors du projet, des temporaires et des caches ; la lecture des secrets listés (~/.ssh, ~/.aws, le trousseau macOS, les dossiers de configuration et de données de tenon, deny_read…) ; le réseau, fermé sauf si une règle ou votre réponse l'ouvre. Les variables qui portent des secrets connus de tenon (api_key_env, bearer_env, env_from) ne sont pas transmises aux commandes, dans tous les modes.
Ce qui reste ouvert.
- Les lectures ailleurs : une commande lit tout ce que vous pouvez lire, sauf les chemins listés. Un secret gardé dans un fichier non listé est lisible.
- Si l'isolation est indisponible et que le mode est
auto, les commandes s'exécutent sans isolation, avec une notice. Utilisezrequiredpour refuser ;strict-locall'impose. - Linux, Landlock : les noms contenus dans un dossier interdit peuvent être listés (le contenu ne peut pas être lu). Un fichier ne peut pas être créé, supprimé ni renommé directement dans un dossier qui contient une entrée interdite, ni dans ses ancêtres jusqu'à la racine inscriptible (exemple :
deny_read = ["<projet>/secrets"]bloque les nouveaux fichiers directement à la racine du projet ; un fichier--configsous/tmpbloque les nouveaux fichiers directement dans/tmp). Les entrées créées après le lancement dans des dossiers non inscriptibles ne sont pas lisibles. - Linux, sockets Unix : confinés seulement avec Landlock ABI 9 ou plus. Sur un noyau plus ancien, un socket accessible par son chemin (par exemple
/var/run/docker.sock) reste connectable ; tenon affichesandbox: Unix sockets not confined (Landlock ABI < 9): <chemin> reachable. Pour un utilisateur du groupedocker, ce socket donne root sur la machine. Le bus de session D-Bus ($XDG_RUNTIME_DIR/bus,/run/user/<uid>/bus, même notice) permet à une commande de lancer un processus non confiné avecsystemd-run --user, ou de lire des secrets dans le Secret Service (GNOME Keyring, KWallet), clés des fournisseurs rangées par tenon comprises.requiredne refuse pas de démarrer pour autant. - Linux, repli TCP (espaces de noms utilisateur indisponibles : profil seccomp par défaut de Docker, restriction AppArmor d'Ubuntu) : UDP reste ouvert, donc DNS et autre trafic UDP peuvent sortir. Les sockets Unix abstraits sont cloisonnés (ABI 6 ou plus). L'isolation complète exige
CLONE_NEWUSER|CLONE_NEWNET. - Linux, git :
.git/configet.git/hooksdu projet restent inscriptibles (une exclusion en écriture seule empêcherait git de créer ses propres fichiers à la racine de.git). Une commande peut y planter un hook ou uncore.fsmonitorexécuté sans isolation à votre prochaine commande git. Sous macOS, la protection est au mieux : dans un dossier.gitordinaire,config,hooks,commondiretconfig.worktreene peuvent être ni écrits ni créés, et ni.gitni le projet ne peuvent être supprimés ou renommés ; les dossiers git des sous-modules (.git/modules/*), les dépôts imbriqués (sous/.git) et un fichier.git(worktree, sous-module) qui pointe hors du projet ne sont pas protégés. - macOS :
sandbox-execest déprécié par Apple, mais fonctionne.(deny network*)couvre aussi les sockets Unix. - macOS, environnement : retirer une variable signifie qu'elle n'est pas transmise, pas qu'elle est illisible. Une commande confinée peut lire l'environnement de vos autres processus, tenon compris, avec
sysctlKERN_PROCARGS2. Gardez les clés des fournisseurs dans le trousseau (interdit aux commandes) plutôt que dans l'environnement. L'interdiction du trousseau rend aussi illisibles depuis une commande confinée les identifiants que gitosxkeychain,ghetdockery gardent :git pushen HTTPS,ghetdocker pullavec identifiants enregistrés échouent sous isolation. - Les caches inscriptibles (
~/.cache,~/Library/Caches,~/.npm,$TMPDIR,write) peuvent être empoisonnés : ce qu'une commande confinée y laisse sert ensuite à des exécutions non confinées (hooks, vos propres commandes). - Un projet sous
/tmp,$TMPDIRou un dossier de cache reste inscriptible pour tout serveur MCP stdio,risk = "read"ou vide compris : la liste d'écriture par défaut le couvre. - Les hooks et le marquage git ne sont pas confinés.
- Un serveur MCP stdio est confiné selon son
risk; son dossier de travail$HOMEn'est pas inscriptible. Un serveur déclarérisk = "network"a le réseau ; un serveur sansriskn'a pas de réseau et voit le projet en lecture seule. - Une commande qui reçoit le réseau (règle
network = true, ou « Autoriser avec réseau ») peut envoyer tout ce qu'elle peut lire. « Oui pour la session » ne mémorise jamais le réseau d'une réponse ; il garde celui d'une règleaskavecnetwork = true. - Un fichier
--configexplicite hors du projet est interdit aux commandes (lecture et écriture). Dans le projet il ne l'est pas : il est versionné. - Seccomp, limites de ressources, espaces de noms PID et montage, filtre d'hôtes réseau et Windows ne sont pas couverts.
Le routage par sensibilité et ses limites¶
La classification est un garde-fou pour le contenu que vous avez listé, pas un système de prévention des fuites de données.
- Elle ne détecte ni données personnelles, ni secrets, ni identifiants : le contenu hors des chemins listés qui ne porte aucun marquage est traité comme normal.
- Les marquages ne sont trouvés que dans du texte, après repli de la casse, des accents et des espaces. Un marquage dans une image, un fichier binaire ou un encodage inhabituel échappe.
- Une commande shell qui lit des données classées sans nommer leur chemin (par une variable, un joker développé par le shell, un
grep -rou unfindrécursif, un extrait deheadousedqui ne contient pas la ligne du marquage) n'est rattrapée que si un marquage apparaît dans sa sortie.~et$VARne sont pas développés quand les commandes sont comparées aux chemins classés. Sous macOS avec l'isolation active, les chemins classés (début littéral de chaque motif, chemins existants seulement) sont interdits aux commandes shell, donc ces lectures échouent ; sous Linux ou sans isolation, ils ne le sont pas. - La sortie shell est contrôlée après troncature à
output_limit: un marquage au-delà de la limite échappe, et le début de la sortie part. globne renvoie que des noms : il ne rend jamais la session sensible et il est exempté du contrôle sortant, donc un marquage dans un nom de fichier peut atteindre un fournisseur distant.- Les marquages dans les définitions d'outils (descriptions MCP) ne sont pas analysés.
- « Local » est une déclaration : un fournisseur de juridiction
localest supposé sous votre contrôle. tenon affiche une notice quand son hôte n'est pas une adresse de boucle locale, et le fournisseur local n'utilise jamais les variables d'environnement de proxy, mais tenon ne peut pas vérifier où tourne le serveur. - Une session devient sensible après la lecture du contenu : celui-ci reste sur la machine (fournisseur local seulement) mais se trouve dans l'historique, dans le journal d'audit avec
audit.level = "full"et dans les points de restauration. - Sans fournisseur local, la classification refuse ou masque ; elle ne peut pas router.
Voir utilisation.md.
La vue du contexte n'est pas une protection¶
La sobriété en tokens (déduplication, masquage, troncature) façonne ce qui part au modèle pour économiser des tokens. Ce n'est pas un contrôle de sécurité.
- La vue est appliquée après le scan sortant et le masquage des secrets : elle ne cache rien que ceux-ci n'aient déjà traité, et ne lève jamais une sensibilité.
- L'historique garde le contenu complet. Le journal d'audit (avec
audit.level = "full"),--resume,/rewind, le routage et les points de restauration l'utilisent, pas la vue. Un résultat d'outil masqué reste dans le journal et dans le fichier de session. - Le masquage protège les résultats des 3 derniers messages assistant portant des appels d'outils ; le texte de l'utilisateur et de l'assistant et le résumé de compaction ne sont jamais touchés.
- Les décomptes de tokens sont réels quand le fournisseur les donne, estimés sinon (marqués). L'énergie et le CO2 n'existent qu'avec des facteurs que vous déclarez.
- Le budget est un plafond souple : des appels de sous-agents en parallèle peuvent le dépasser un peu.
Voir utilisation.md.
La mémoire de projet n'est pas digne de confiance¶
Une entrée de .tenon/memory d'un dépôt est écrite par les auteurs du dépôt, comme AGENTS.md. La mémoire entre dans le contexte du modèle sans être vue et persiste d'une session à l'autre : c'est un vecteur d'injection d'instructions.
tenon ne charge donc une entrée de projet que si son empreinte est approuvée sur ce poste (tenon memory trust, qui montre d'abord le contenu). Un fichier modifié n'est plus approuvé jusqu'à votre accord. Une entrée non approuvée est absente de l'index et memory_read la refuse.
L'index dit au modèle que les entrées sont des notes, pas des instructions. C'est une indication, pas un filtre : tenon n'analyse pas le texte d'une entrée approuvée. Lisez ce que vous approuvez.
Un lien symbolique dans .tenon/memory (ou le dossier lui-même) n'est jamais suivi.
Ce que la mémoire ne peut pas faire : relever les permissions ou changer une règle. memory_write et memory_forget demandent une approbation comme toute écriture, et les règles comparent <portée>/<id>.
Une règle allow sur user/* ou global/* supprime cette approbation. Avec une telle règle, un texte injecté dans la conversation (un fichier, une page web, un résultat MCP) peut faire enregistrer une entrée au modèle sans approbation. Une entrée user ou global est toujours approuvée : elle figure dans l'index des sessions suivantes, et l'injection persiste. Gardez ask pour ces portées sauf si vous acceptez ce risque, et vérifiez tenon memory list.
Entrées sensibles. Une entrée avec un marqueur de classification, ou écrite dans une session sensible, n'est écrite qu'en portée user : rien de classé n'arrive dans un fichier que git versionne. Son titre n'est jamais dans l'index. La lire rend la session sensible ; sans fournisseur local, la lecture est refusée et le contenu ne quitte pas la machine. Le masquage avant écriture ne lève jamais la sensibilité d'une session.
Ce qui reste après tenon memory forget.
- L'historique git garde une entrée
projectversionnée. tenon affichememory: <id> removed from the working tree; git history still holds it. - Le journal d'audit est chaîné et garde l'id, la portée, l'empreinte et l'auteur, jamais le titre ni le contenu.
- La conversation (arguments des appels d'outils, messages de l'assistant) est enregistrée comme celle de tout appel d'outil, selon le niveau d'audit et avec le masquage. Oublier une entrée n'efface pas les sessions passées ni leurs lignes d'audit.
Les entrées personnelles (user, global) sont de simples fichiers de votre dossier de données, sans chiffrement au repos : qui peut lire le dossier peut les lire. Elles expirent après retention_days (90 par défaut).
Voir utilisation.md.
Les skills de projet ne sont pas dignes de confiance¶
Un skill placé dans .tenon/skills, .claude/skills ou .agents/skills d'un dépôt est un contenu écrit par les auteurs du dépôt, comme AGENTS.md.
Son texte peut contenir des instructions destinées à tromper le modèle (injection d'instructions), et le modèle peut le charger de lui-même quand la description correspond.
tenon ne filtre pas ce texte.
Ce qu'un skill ne peut pas faire :
- Il ne peut pas élargir les permissions.
allowed-toolsest lu, affiché et journalisé, mais ne change jamais une décision. - Ses scripts ne sont pas exécutés par le système de skills : ils passent par l'outil
shell, avec la validation habituelle, et toute commande qu'aucune règleallowne couvre demande votre validation. - Il ne peut pas masquer un skill utilisateur : à nom égal, le skill utilisateur l'emporte et un avis signale le skill de projet comme ignoré.
- Ses fichiers sont lus avec les mêmes protections que les autres fichiers : pas de lien symbolique, pas de chemin hors du dossier du skill, pas de fichier sensible.
Ce que vous devez faire : lire les skills d'un dépôt inconnu avant d'utiliser le projet, comme vous liriez AGENTS.md.
tenon skills liste les skills trouvés par tenon, avec leur source. Une règle deny sur tool = "skill" en interdit un.
Chaque chargement figure dans le journal d'audit avec le SHA-256 du fichier lu (voir utilisation.md).
Les sous-agents agissent avec les droits de la session¶
Un sous-agent s'exécute avec les mêmes permissions, le même masquage, le même accord de juridiction et le même audit que l'agent principal.
Chacune de ses actions passe par les mêmes règles et les mêmes validations. Ses actions figurent dans le même journal d'audit, dans la même chaîne.
Il n'obtient jamais plus de droits que la session, et il ne peut pas lancer de sous-agent.
Le champ model d'un fichier d'agent est ignoré : un sous-agent utilise le fournisseur et le modèle de la session, donc il n'ajoute aucune nouvelle juridiction.
Un fichier placé dans .tenon/agents, .claude/agents ou .agents/agents d'un dépôt est un contenu non fiable, comme AGENTS.md et les skills de projet.
Son texte devient le prompt système d'un sous-agent, et peut contenir des instructions destinées à tromper le modèle. tenon ne filtre pas ce texte.
Un agent de projet ne peut pas masquer un agent utilisateur, et tools ne peut que restreindre les outils de la session.
Ce que vous devez faire : lire les agents d'un dépôt inconnu avant d'utiliser le projet.
Un fichier de projet nommé general-purpose remplace l'agent intégré, qui est la cible par défaut de l'outil agent : un dépôt peut donc changer les instructions et les outils de toute délégation qui ne nomme pas d'agent.
Les approbations « oui pour la session » sont partagées entre l'agent principal et ses sous-agents : une approbation donnée pour une action d'un sous-agent vaut aussi pour l'agent principal, et inversement.
tenon agents liste les agents trouvés par tenon, avec leur source. Une règle deny sur tool = "agent" en interdit un (voir utilisation.md).
Les hooks exécutent votre code, avec vos droits¶
Un hook est un programme que vous avez écrit ou installé. Il s'exécute sous votre identité, hors des règles de permission.
Seuls vos propres fichiers sont lus (~/.config/tenon/hooks.json, et ~/.claude/settings.json si vous mettez hooks.claude_settings = true). Les hooks d'un projet ne s'exécutent jamais : tenon affiche seulement un avis. Un dépôt ne peut pas vous faire exécuter de code par un hook qui lui appartient. Mais un de vos hooks qui exécute un fichier du projet ($CLAUDE_PROJECT_DIR/.claude/hooks/x.sh, ./scripts/..., npm test) exécute du code du dépôt, avec vos droits : n'écrivez de tels hooks que pour des dépôts de confiance.
Un hook ne peut pas élargir les permissions de la session. allow lève seulement un ask issu des règles, comme le ferait une règle allow globale. Il ne lève jamais une règle deny, une action protégée, un chemin hors du projet, un fichier sensible ni une règle ask du projet. Un hook peut toujours restreindre : deny, ask, ou une modification de l'entrée de l'outil, qui est revalidée.
Les hooks command reçoivent l'entrée de l'outil non masquée : ils s'exécutent sur votre machine, et tenon ne contrôle pas ce qu'ils en font. Un hook qui envoie cette entrée à un service (par exemple un hook de notification qui poste vers un service hors UE) relève de votre responsabilité, comme un serveur MCP stdio. Le contrôle de juridiction ne le couvre pas.
Les hooks prompt et agent, et tout ce qu'un hook renvoie au modèle (additionalContext, raisons, stderr), passent par le même masquage et le même contrôle de juridiction que toute requête. Un hook prompt ou agent utilise le fournisseur et le modèle de la session (model est ignoré), donc il n'ajoute aucune nouvelle juridiction.
Un hook en échec ne bloque pas. Un plantage, un délai, une sortie illisible ou une erreur du fournisseur est une erreur non bloquante : l'action continue, avec un avertissement dans l'interface et dans le journal d'audit. Un hook de sécurité cassé ne vous protège donc pas. Testez vos hooks et surveillez les avertissements. Seul un fichier de hooks invalide empêche tenon de démarrer.
Chaque exécution de hook est dans le journal d'audit (voir utilisation.md). tenon hooks liste les hooks chargés par tenon.
Les points de restauration gardent le contenu brut de vos fichiers¶
Avant que write_file et edit_file écrasent un fichier, tenon en garde une copie pour que /rewind puisse le restaurer. La copie est le contenu brut, secrets compris (.env, clés, jetons) : la restauration exige le fichier exact.
Les copies sont dans ~/.local/share/tenon/checkpoints/, hors du projet, donc elles ne peuvent pas être commitées par erreur. Les dossiers sont en 0700 et les fichiers en 0600. Elles ne sont jamais envoyées au fournisseur et jamais écrites dans la session ni dans le journal d'audit, qui ne contiennent que les empreintes. Le masquage ne s'y applique pas.
Quiconque peut lire votre dossier de données peut lire ces copies. Elles sont gardées retention_days jours (30 par défaut). Pour toutes les supprimer maintenant : rm -rf ~/.local/share/tenon/checkpoints. retention_days = 0 arrête leur enregistrement.
Un rewind n'écrit que des chemins que les outils ont écrits dans la session. Il ne suit jamais un lien symbolique dans le dernier composant d'un chemin : un chemin devenu lien est un conflit, et n'est pas écrit. Un fichier que vous avez modifié après l'agent n'est pas écrasé sans votre réponse explicite. Chaque copie est vérifiée avec son sha256 avant d'être restaurée. write_file et edit_file n'écrasent jamais un fichier existant qu'ils ne peuvent pas lire.
Un rewind est une action de l'utilisateur : aucun outil et aucun hook ne peut le déclencher. Il n'annule pas les commandes shell ni les outils MCP (voir utilisation.md).
La juridiction est déclarative¶
tenon ne vérifie pas la juridiction d'un fournisseur ou d'un serveur.
Il applique la valeur écrite dans la configuration.
Une erreur de déclaration (eu pour un opérateur soumis au CLOUD Act) supprime la protection.
Par prudence, un fournisseur ajouté sans jurisdiction est traité comme non-eu.
Serveur stdio local¶
Un serveur MCP stdio vaut local par défaut.
Cela décrit l'endroit où tourne le programme, pas l'endroit où il envoie ensuite vos données.
Si le programme relaie un service hors de l'UE, déclarez-le jurisdiction = "non-eu".
Le masquage est approximatif¶
- La détection repose sur des expressions régulières. Elle oublie des secrets au format inconnu et masque parfois du texte ordinaire.
- La détection des données personnelles est partielle. tenon ne garantit pas la conformité au RGPD.
- La réponse du modèle arrive par fragments (
AssistantDelta). Chaque fragment est masqué séparément : un secret coupé entre deux fragments peut apparaître en clair dans les fragments du journal d'audit et de la sortie--output json. Le message final complet (AssistantMessage) est masqué d'un seul tenant. - Une clé d'API ou une valeur
env_from/bearer_envde moins de 8 caractères n'est pas masquée telle quelle, pour ne pas masquer du texte ordinaire.
Ajoutez vos propres règles avec [[redaction.rules]].
Horodatage (RFC 3161)¶
La menace : quelqu'un qui contrôle la machine réécrit tout le journal et recalcule chaque hash. La chaîne seule ne le voit pas.
- Avec
[audit.timestamp], une autorité d'horodatage (TSA) signe le hash de tête à la fin de chaque session ou partenon audit timestamp. Un jeton prouve que le journal jusqu'à la ligne couverte existait à l'instant signé. Un journal réécrit ensuite ne contient plus le hash couvert par un jeton :tenon audit verifyettenon audit exportle signalent (anchored hash not in chain), à condition que les jetons soient encore là. Quiconque contrôle la machine peut aussi supprimeraudit/timestamps/:audit verifyaffiche alorshorodatages : aucunet sort avec le code 0. Copiez régulièrement les jetons (et le journal) hors de la machine, comme le dernier hash (voir plus bas), et faites comparer les auditeurs à ces copies ou au nombre de jetons attendu. - La TSA ne reçoit qu'un hash SHA-256, jamais de contenu. tenon n'intègre aucun préréglage de TSA : vous choisissez l'autorité, et rien n'est appelé sans configuration.
- Un jeton ne prouve pas les lignes qui le suivent (
lignes après : <m>dans la sortie deaudit verify), ni que le contenu était vrai. La suppression des dernières lignes n'est détectée que si un jeton couvre une ligne plus tardive. - La révocation (CRL, OCSP) n'est pas contrôlée : la vérification est hors ligne. Un certificat de TSA révoqué après la signature se vérifie encore.
- Le client de TSA ne suit pas les redirections : une redirection
httpsvershttpne contourne pas le contrôle de l'URL. - Sous macOS et Windows, les racines système ne permettent pas de vérifier les certificats de TSA : utilisez
ca_file,cade la politique ou--tsa-ca. Sinon l'ancrage de fin de session échoue avec une notice (jamais une erreur). - Vérifier sans tenon :
openssl ts -verify -digest <hex> -in <jeton>.tsr -token_in -CAfile <ca>,<hex>étant le hash de tête sans le préfixesha256:. - Un ancrage en échec ne bloque jamais une session et ne change jamais le code de sortie.
Configuration, commande et sortie : voir utilisation.md.
Troncature de fin du journal¶
Supprimer les dernières lignes du journal ne casse pas la chaîne : tenon audit verify ne peut pas le détecter.
Parade : notez régulièrement le dernier hash affiché par tenon audit verify hors de la machine (ticket, dépôt séparé, message signé).
Plus tard, vérifiez que ce hash figure toujours dans le journal, par exemple avec grep.
S'il a disparu, des lignes ont été retirées.
Un jeton d'horodatage couvre aussi le journal jusqu'à sa ligne : voir Horodatage.
Sosies visuels et terminal¶
Le panneau de validation rend visibles les caractères invisibles, de largeur nulle, bidirectionnels et les espaces inhabituelles ([U+XXXX]).
Il ne peut pas tout montrer :
- les lettres sosies (cyrillique
аau lieu de latina, faux guillemets, faux tirets) restent affichées telles quelles. Un avertissement signale seulement qu'une commande shell contient un caractère non ASCII ; - un domaine sosie dans une URL n'est pas détecté ;
- certains terminaux réordonnent le texte bidirectionnel ou dessinent mal les marques combinantes : le rendu dépend aussi de votre terminal et de sa police.
Lisez la commande. En cas de doute, refusez.
Marquage git¶
- Le marquage repose sur les fichiers de session. Une session supprimée n'est plus prise en compte.
- La correspondance se fait par chemin : un fichier écrit une fois par l'agent est marqué à chaque commit où il est indexé, même si un humain l'a entièrement réécrit depuis.
git commit --no-verify, un hook absent outenonabsent duPATHdonnent un commit non marqué. Les commits de fusion ne sont pas marqués.
Le marquage est une aide à la traçabilité, pas une preuve.
Outils de build et réseau¶
Compiler tenon depuis les sources ne contacte aucun service : les dépendances sont vendorées.
La chaîne de développement (make lint, make vuln, make release, CI) fait exception :
- les outils de build épinglés (staticcheck et govulncheck, installés dans
.tools/parscripts/go-tool.sh; cyclonedx-gomod et nfpm, lancés pargo run <module>@<version>) sont téléchargés depuis leurs hôtes d'origine (GOPROXY=direct) :honnef.co(qui renvoie versgithub.com),golang.orgetgo.googlesource.com(opéré par Google),github.com(opéré par Microsoft) ; - leurs empreintes sont vérifiées auprès de
sum.golang.org, opéré par Google ; govulncheckinterroge la base de vulnérabilitésvuln.go.dev, opérée par Google ;- le dépôt et la CI GitHub Actions sont hébergés par GitHub (Microsoft, États-Unis) ; les fichiers
.forgejo/et.woodpecker.ymlpermettent une CI sur une forge européenne ou auto-hébergée ; - la CI (
.github/workflows/ci.yml,.forgejo/workflows/ci.yml,.woodpecker.yml) télécharge ses images depuis Docker Hub (Docker Inc., États-Unis) :golanget, pour Woodpecker,woodpeckerci/plugin-git. Ces images sont épinglées à une version ; un miroir de registre européen peut les remplacer.
Ces services ne reçoivent aucune donnée de vos sessions.
Avec la configuration par défaut de Go (sans GOPROXY=direct), les modules passent aussi par proxy.golang.org, opéré par Google.
Récupération¶
Journal d'audit corrompu¶
Symptômes : tenon audit verify signale fichier:ligne, ou tenon refuse de démarrer avec « last log line unreadable, chain not resumed » (dernière ligne du journal illisible, chaîne non reprise).
Ne modifiez pas le journal en place : il sert de preuve.
- Quittez toutes les instances de tenon.
- Mettez le journal de côté, sans le modifier :
mkdir -p ~/tenon-audit-2026-10-15/tenon mv ~/.local/share/tenon/audit ~/tenon-audit-2026-10-15/tenon/audit - Vérifiez la copie pour noter l'endroit exact de la faute :
XDG_DATA_HOME=~/tenon-audit-2026-10-15 tenon audit verify - Relancez tenon : un nouveau journal commence avec
prev = "sha256:0". - Consignez l'incident (date, faute signalée, dernier hash connu) à côté de la copie.
Une dernière ligne coupée (arrêt brutal pendant une écriture) produit le même symptôme. La procédure reste la même.
Session corrompue¶
À la reprise (tenon --resume), tenon lit la session jusqu'à la dernière ligne lisible et affiche un avertissement.
Le fichier est coupé à la première ligne illisible.
L'original complet est gardé dans ~/.local/share/tenon/sessions/<id>.jsonl.corrupt.
Si la reprise échoue malgré tout, démarrez une nouvelle session. Le contenu de l'ancienne reste lisible dans son fichier JSONL et dans le journal d'audit.
Une session « déjà ouverte dans un autre terminal » est verrouillée par un autre processus tenon. Fermez-le avant de reprendre la session.