[{"data":1,"prerenderedAt":30},["ShallowReactive",2],{"doc:en:plugins\u002Fpermission-rules":3},{"title":4,"lede":5,"html":6,"headings":7},"Permission rules","A plugin can decide the permissions your agents ask for before they reach you: allow a safe command, deny a dangerous one, and leave you the rest. The requests that interrupt you most become rules, without switching to Bypass mode.","\u003Cp>It is a plugin’s strongest permission: it is shown \u003Cstrong>in red\u003C\u002Fstrong> at install (“Decides your agents’ permissions”), each of its decisions goes to its journal, and no project can give it: an \u003Ccode class=\"key\">.escouade.yml\u003C\u002Fcode> never turns a plugin on.\u003C\u002Fp>\n\u003Ch2 id=\"how-escouade-decides\">How Escouade decides\u003C\u002Fh2>\n\u003Cp>When an agent asks for a permission (a command, a file change, a web page, an MCP tool), whatever its harness:\u003C\u002Fp>\n\u003Col>\n\u003Cli>Escouade asks every plugin that has \u003Ccode class=\"key\">permissions.decide\u003C\u002Fcode> and is on for the agent’s project, \u003Cstrong>all at once\u003C\u002Fstrong>.\u003C\u002Fli>\n\u003Cli>Each has \u003Cstrong>500 ms\u003C\u002Fstrong> to answer: \u003Ccode class=\"key\">allow\u003C\u002Fcode>, \u003Ccode class=\"key\">deny\u003C\u002Fcode> with a message, or \u003Ccode class=\"key\">ask\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>A denial wins\u003C\u002Fstrong>, whatever the order of the answers. Otherwise one allowance is enough. Otherwise (they all answered \u003Ccode class=\"key\">ask\u003C\u002Fcode>, kept silent, crashed or answered something else) the request goes to the user as it would without plugins.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Meanwhile the request’s card is already in the conversation, but Escouade does not notify you: you are called only if no plugin settled it. You can still answer on the card yourself; whichever answer comes first is kept.\u003C\u002Fp>\n\u003Cp>A plan to approve (\u003Ccode class=\"key\">ExitPlanMode\u003C\u002Fcode>) and a question of the agent never go to the plugins: they are yours.\u003C\u002Fp>\n\u003Ch2 id=\"what-the-agent-sees-what-you-see\">What the agent sees, what you see\u003C\u002Fh2>\n\u003Cp>The card of a request a plugin settled says who answered: “\u003Cstrong>Allowed by Team rules\u003C\u002Fstrong> via Escouade”, or “\u003Cstrong>Denied by Team rules\u003C\u002Fstrong> via Escouade — no rm -rf here”.\u003C\u002Fp>\n\u003Cp>The agent learns who refused, never that you did: it is told “Refused by Team rules through Escouade: no rm -rf here”, and can look for another way.\u003C\u002Fp>\n\u003Ch2 id=\"the-request\">The request\u003C\u002Fh2>\n\u003Cp>Escouade calls \u003Ccode class=\"key\">permissions\u002Fdecide\u003C\u002Fcode> (see \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Fapi\u002F\">The protocol\u003C\u002Fa>):\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;request&quot;: {\n    &quot;id&quot;: &quot;req_perm&quot;,\n    &quot;kind&quot;: &quot;command&quot;,\n    &quot;tool&quot;: &quot;Bash&quot;,\n    &quot;input&quot;: { &quot;command&quot;: &quot;rm -rf build&quot;, &quot;description&quot;: &quot;Removes the build folder&quot; }\n  },\n  &quot;project&quot;: { &quot;id&quot;: &quot;8c1f…&quot;, &quot;name&quot;: &quot;Escouade&quot;, &quot;private&quot;: false },\n  &quot;ticket&quot;: { &quot;id&quot;: &quot;e02a…&quot;, &quot;key&quot;: &quot;ESC-180&quot;, &quot;title&quot;: &quot;Permission rules&quot; },\n  &quot;agent&quot;: { &quot;id&quot;: &quot;5b7d…&quot;, &quot;name&quot;: &quot;esc-180-permission-rules&quot; }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Ccode class=\"key\">request.tool\u003C\u002Fcode>: the tool, named as Claude Code names it, \u003Cstrong>whatever the harness\u003C\u002Fstrong> (Codex, Copilot CLI, OpenCode, Cursor CLI, Mistral Vibe, Antigravity CLI and Gemini CLI are translated): \u003Ccode class=\"key\">Bash\u003C\u002Fcode>, \u003Ccode class=\"key\">Edit\u003C\u002Fcode>, \u003Ccode class=\"key\">Write\u003C\u002Fcode>, \u003Ccode class=\"key\">Read\u003C\u002Fcode>, \u003Ccode class=\"key\">WebFetch\u003C\u002Fcode>, \u003Ccode>mcp__&lt;server&gt;__&lt;tool&gt;\u003C\u002Fcode>…\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">request.kind\u003C\u002Fcode>: its family, for a simple rule: \u003Ccode class=\"key\">command\u003C\u002Fcode> (\u003Ccode class=\"key\">Bash\u003C\u002Fcode>), \u003Ccode class=\"key\">edit\u003C\u002Fcode> (\u003Ccode class=\"key\">Edit\u003C\u002Fcode>, \u003Ccode class=\"key\">Write\u003C\u002Fcode>, \u003Ccode class=\"key\">MultiEdit\u003C\u002Fcode>, \u003Ccode class=\"key\">NotebookEdit\u003C\u002Fcode>), \u003Ccode class=\"key\">read\u003C\u002Fcode> (\u003Ccode class=\"key\">Read\u003C\u002Fcode>, \u003Ccode class=\"key\">Grep\u003C\u002Fcode>, \u003Ccode class=\"key\">Glob\u003C\u002Fcode>, \u003Ccode class=\"key\">LS\u003C\u002Fcode>), \u003Ccode class=\"key\">fetch\u003C\u002Fcode> (\u003Ccode class=\"key\">WebFetch\u003C\u002Fcode>, \u003Ccode class=\"key\">WebSearch\u003C\u002Fcode>), \u003Ccode class=\"key\">mcp\u003C\u002Fcode> (an MCP tool) or \u003Ccode class=\"key\">tool\u003C\u002Fcode> (the rest).\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">request.input\u003C\u002Fcode>: what the tool was given, its long texts cut.\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">project\u003C\u002Fcode>, \u003Ccode class=\"key\">ticket\u003C\u002Fcode> (absent outside a ticket), \u003Ccode class=\"key\">agent\u003C\u002Fcode>: as in \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Fevents\u002F\">a fact\u003C\u002Fa>. Never the conversation.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"the-answer\">The answer\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-json\">{ &quot;decision&quot;: &quot;deny&quot;, &quot;message&quot;: &quot;no rm -rf here&quot; }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003Ccode class=\"key\">decision\u003C\u002Fcode>\u003C\u002Fth>\n\u003Cth>What happens\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">allow\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The request is allowed, once, unless another plugin denies it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">deny\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>It is denied; \u003Ccode class=\"key\">message\u003C\u002Fcode> (optional) is told to the agent and shown on the card.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ask\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The plugin leaves it: the request goes to the user unless another one settles it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>A JSON-RPC error, an answer past 500 ms or an answer that is none of the three count as \u003Ccode class=\"key\">ask\u003C\u002Fcode>. A plugin that crashes during a decision is restarted as usual; the request goes to the user.\u003C\u002Fp>\n\u003Cp>Read your rules \u003Cstrong>before\u003C\u002Fstrong> the first request (at start), not during one: 500 ms is short. Escouade starts the plugins that decide when it starts, and as soon as one is installed, updated or switched back on; a plugin not started yet has, once, three seconds to start and answer.\u003C\u002Fp>\n\u003Ch2 id=\"an-example\">An example\u003C\u002Fh2>\n\u003Cp>“Team rules” denies what \u003Ccode class=\"key\">deny\u003C\u002Fcode> forbids, allows what \u003Ccode class=\"key\">allow\u003C\u002Fcode> permits, and leaves the rest to the user. Its manifest asks only for \u003Ccode>&quot;permissions&quot;: { &quot;decide&quot;: true }\u003C\u002Fcode>; its rules are in a file of its folder:\u003C\u002Fp>\n\u003Cp>\u003Ccode>plugins\u002Fteam-rules\u002Frules.json\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;deny&quot;: [\n    &quot;rm -rf&quot;,\n    &quot;git reset --hard&quot;,\n    { &quot;command&quot;: &quot;git push --force&quot;, &quot;message&quot;: &quot;No force push in this team: push a new commit instead.&quot; },\n    { &quot;command&quot;: &quot;git push -f&quot;, &quot;message&quot;: &quot;No force push in this team: push a new commit instead.&quot; }\n  ],\n  &quot;allow&quot;: [&quot;npm test&quot;, &quot;npm run lint&quot;, &quot;cargo test&quot;, &quot;cargo nextest&quot;, &quot;git status&quot;, &quot;git diff&quot;, &quot;git log&quot;]\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Its program, written with the \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Fsdk\u002F\">SDK\u003C\u002Fa>, reads its rules from \u003Ccode class=\"key\">rules.json\u003C\u002Fcode> and judges each command of a line, even after a \u003Ccode class=\"key\">&amp;&amp;\u003C\u002Fcode>. Its whole code and its tests are in \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Fexamples\u002F#team-rules\">Example plugins\u003C\u002Fa>.\u003C\u002Fp>\n\u003Cp>The bench checks it: \u003Ccode>escouade-plugin test\u003C\u002Fcode> plays a decision and measures its delay, and \u003Ccode>escouade-plugin dev\u003C\u002Fcode> lets you play some by hand (\u003Ccode>decide {&quot;request&quot;: {...}}\u003C\u002Fcode>). See \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Ftesting\u002F\">Testing a plugin\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"the-journal\">The journal\u003C\u002Fh2>\n\u003Cp>Each decision is written in the plugin’s journal (Plugins › the plugin › Journal, filter “Decision”): the tool and what it aimed at, the answer, its delay, and why it did not decide (no answer in time, an error, a crash).\u003C\u002Fp>\n\u003Cpre>\u003Ccode>14:03:12  Decision  refused Bash rm -rf build (2 ms): no rm -rf here\n14:05:40  Decision  allowed Bash npm test (1 ms)\n14:07:02  Decision  did not decide Bash cargo build: no answer in time; the user is asked\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"taking-it-back\">Taking it back\u003C\u002Fh2>\n\u003Cp>In \u003Cstrong>Plugins\u003C\u002Fstrong>, on the plugin’s page:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>take back the permission “Decides your agents’ permissions”: it goes on without it and is no longer asked;\u003C\u002Fli>\n\u003Cli>turn it off for a project: its agents are no longer submitted to it;\u003C\u002Fli>\n\u003Cli>a \u003Cstrong>private\u003C\u002Fstrong> project is never submitted to a plugin that is not turned on for it.\u003C\u002Fli>\n\u003C\u002Ful>\n",[8,12,15,18,21,24,27],{"id":9,"text":10,"depth":11},"how-escouade-decides","How Escouade decides",2,{"id":13,"text":14,"depth":11},"what-the-agent-sees-what-you-see","What the agent sees, what you see",{"id":16,"text":17,"depth":11},"the-request","The request",{"id":19,"text":20,"depth":11},"the-answer","The answer",{"id":22,"text":23,"depth":11},"an-example","An example",{"id":25,"text":26,"depth":11},"the-journal","The journal",{"id":28,"text":29,"depth":11},"taking-it-back","Taking it back",1793889382293]