[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"doc:en:plugins\u002Fsecurity":3},{"title":4,"lede":5,"html":6,"headings":7},"Plugin security","A plugin from the store goes through three guards: the store’s eleven checks on submission, the “Verified by Escouade” review done by hand, and the app’s checks on every install. This page says what each one holds, what the review refuses, and what the seal does not promise.","\u003Ch2 id=\"what-the-app-checks\">What the app checks\u003C\u002Fh2>\n\u003Cp>On install and on every update, Escouade downloads \u003Ccode class=\"key\">version.json\u003C\u002Fcode>, the archive and its WASM module, then checks:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>the fingerprints\u003C\u002Fstrong>: \u003Ccode class=\"key\">version.json\u003C\u002Fcode> against the catalog, the archive, the module and the manifest against \u003Ccode class=\"key\">version.json\u003C\u002Fcode> (SHA-256). One byte changed, on the store, on the way or in a cache, stops the install;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>the signature\u003C\u002Fstrong>, for a verified version: \u003Ccode class=\"key\">version.json\u003C\u002Fcode> signed with minisign by Escouade’s key, the signature’s comment naming this id and this number. The public keys are in the app; the secret key lives on one machine, encrypted, never on the store’s server, never in the CI or in a repository. So the store cannot make a seal;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>the archive\u003C\u002Fstrong>, read in memory, without a symbolic link or a path that leaves the plugin’s folder;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>the folder\u003C\u002Fstrong> \u003Ccode class=\"key\">version.json\u003C\u002Fcode> names for a plugin kept in a subfolder of its repository: a plain relative path, without \u003Ccode class=\"key\">..\u003C\u002Fcode>, an absolute path or a backslash; otherwise nothing is installed. The store, for its part, refuses at submission a folder that goes through a symbolic link.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>An installed plugin’s seal is checked again on your machine, without the store. Then the install window shows its permissions in words, before anything runs (\u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Finstaller\u002F\">installing a plugin\u003C\u002Fa>).\u003C\u002Fp>\n\u003Ch2 id=\"what-the-seal-covers\">What the seal covers\u003C\u002Fh2>\n\u003Cp>“Verified by Escouade” stands for \u003Cstrong>one version\u003C\u002Fstrong>: its code at the published commit, its archive and its module, recompiled by the review, named by the fingerprint of its \u003Ccode class=\"key\">version.json\u003C\u002Fcode>. A new version is “not verified” until its own review, even if the one before was: an update is where an honest plugin can change hands. \u003Cstrong>Install verified versions only\u003C\u002Fstrong>, in the Plugins view, refuses any version without a seal.\u003C\u002Fp>\n\u003Cp>The seal says that a person read this code against what it declares, and that the bench ran it without network. It does not promise there is no bug, nor the quality of the third-party service the plugin reaches.\u003C\u002Fp>\n\u003Ch2 id=\"what-the-review-refuses\">What the review refuses\u003C\u002Fh2>\n\u003Cp>The review reads each version with a script that prepares leads for it, file and line, and an agent that reads the code as data. Its outcomes: “Verify”, “Request changes” (the version stays published, not verified, the message says what to fix) or “Refuse”.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Refused outright\u003C\u002Fstrong>, without reading further:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>an archive or a module whose fingerprint is not the one in \u003Ccode class=\"key\">version.json\u003C\u002Fcode>;\u003C\u002Fli>\n\u003Cli>a version that is no longer the one the store froze.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>No seal until it is fixed\u003C\u002Fstrong>:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>a host reached that is not in \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode> (a hard-coded address, a \u003Ccode class=\"key\">fetch\u003C\u002Fcode>, a \u003Ccode class=\"key\">WebSocket\u003C\u002Fcode>);\u003C\u002Fli>\n\u003Cli>reading secrets or personal folders beyond what the plugin declares: environment variables, \u003Ccode class=\"key\">~\u002F.ssh\u003C\u002Fcode>, \u003Ccode class=\"key\">~\u002F.claude\u003C\u002Fcode>, the keychain outside \u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode>;\u003C\u002Fli>\n\u003Cli>files read or written outside the plugin’s folder and what its permissions open;\u003C\u002Fli>\n\u003Cli>a program started that the permissions do not announce (\u003Ccode class=\"key\">child_process\u003C\u002Fcode>, \u003Ccode class=\"key\">spawn\u003C\u002Fcode>, \u003Ccode class=\"key\">exec\u003C\u002Fcode>);\u003C\u002Fli>\n\u003Cli>dynamic code: \u003Ccode class=\"key\">eval\u003C\u002Fcode>, \u003Ccode>new Function\u003C\u002Fcode>, an \u003Ccode class=\"key\">import()\u003C\u002Fcode> of an address;\u003C\u002Fli>\n\u003Cli>unreadable code: minified without its source, text hidden in base64, binary blobs;\u003C\u002Fli>\n\u003Cli>unpinned dependencies, a \u003Ccode class=\"key\">postinstall\u003C\u002Fcode> script, a dependency pulled from a git repository;\u003C\u002Fli>\n\u003Cli>instructions added to the agents that turn them from their task (a prompt injection);\u003C\u002Fli>\n\u003Cli>a module the recompilation does not give back, to the byte;\u003C\u002Fli>\n\u003Cli>a Rust dependency that cannot be justified: what \u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode> adds is code that runs; \u003Ccode class=\"key\">unsafe\u003C\u002Fcode> code, an \u003Ccode class=\"key\">extern\u003C\u002Fcode> or a \u003Ccode class=\"key\">#[link]\u003C\u002Fcode> without a reason;\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">agentHome\u003C\u002Fcode>, or an entry in \u003Ccode class=\"key\">*\u003C\u002Fcode> of \u003Ccode class=\"key\">permissions.commands\u003C\u002Fcode>, without a reason that was read;\u003C\u002Fli>\n\u003Cli>a tag moved since the submission, an archive entry refused (link, \u003Ccode class=\"key\">..\u003C\u002Fcode>, absolute path);\u003C\u002Fli>\n\u003Cli>a conformance bench that fails, or that tries the network or a write the sandbox refuses.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>Withdrawn\u003C\u002Fstrong>: a malicious behavior found (sending data elsewhere, hiding what the code does, lying about its permissions) leads to the version’s withdrawal, and the plugin’s if need be.\u003C\u002Fp>\n\u003Ch2 id=\"withdrawal-and-the-revocation-list\">Withdrawal and the revocation list\u003C\u002Fh2>\n\u003Cp>Escouade reads a \u003Cstrong>revocation list\u003C\u002Fstrong> signed by the same key, at startup then every 6 hours, and refuses a list older than the last one read. It has two kinds of entries:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>remove the seal\u003C\u002Fstrong>: the plugin keeps running, a banner says why;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>emergency withdrawal\u003C\u002Fstrong>: the app stops the plugin, disables it and says why, with “Uninstall”. It deletes nothing on its own: your data is yours.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The list is also copied to escouade.dev, an hour at most after the store: if the store is down, the app reads it there. The last list read applies offline.\u003C\u002Fp>\n\u003Ch2 id=\"reporting-a-plugin\">Reporting a plugin\u003C\u002Fh2>\n\u003Cp>“Report”, on a plugin’s page on the store or in the app, is open to anyone with an email address, without a GitHub account: malicious, does not do what it says, misleading permissions, license, other. A person reads every report and answers you. The author sees that a report is open, never who made it.\u003C\u002Fp>\n\u003Ch2 id=\"a-plugin-from-outside-the-store\">A plugin from outside the store\u003C\u002Fh2>\n\u003Cp>A plugin installed from a folder or a git repository has passed neither the checks nor the review: it is \u003Cstrong>not verified\u003C\u002Fstrong>, and the install window says so first. Its permissions apply the same way.\u003C\u002Fp>\n",[8,12,15,18,21,24],{"id":9,"text":10,"depth":11},"what-the-app-checks","What the app checks",2,{"id":13,"text":14,"depth":11},"what-the-seal-covers","What the seal covers",{"id":16,"text":17,"depth":11},"what-the-review-refuses","What the review refuses",{"id":19,"text":20,"depth":11},"withdrawal-and-the-revocation-list","Withdrawal and the revocation list",{"id":22,"text":23,"depth":11},"reporting-a-plugin","Reporting a plugin",{"id":25,"text":26,"depth":11},"a-plugin-from-outside-the-store","A plugin from outside the store",1793889382538]