From b8f551da3a84b0969ecb17198f4445b858389326 Mon Sep 17 00:00:00 2001 From: Hector Fitch Date: Mon, 20 Jul 2026 10:23:13 +0100 Subject: [PATCH] Scope SessionStart hook to Copilot Studio projects only The SessionStart hook has no matcher, so hooks/system-prompt.md (~11KB) is injected into every Claude Code / Copilot CLI session, including ones unrelated to Copilot Studio. This adds constant context overhead and applies the prompt's "bias toward assuming Copilot Studio" and "using skills directly is FORBIDDEN" directives in non-CS sessions. Gate the injection on the plugin's own definition of a CS project: an agent.mcs.yml in the working directory or a subdirectory. A SessionStart hook runs before any user message, so the project marker is the only reliable signal of intent. When no marker is found, print-prompt.js exits without output. hooks.json's inline `command` variant is repointed at the (now gated) print-prompt.js so the gate is the single source of truth across all variants. setup.js is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- hooks/hooks.json | 2 +- hooks/print-prompt.js | 44 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/hooks/hooks.json b/hooks/hooks.json index d034f7f..7f62c8b 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -5,7 +5,7 @@ "hooks": [ { "type": "command", - "command": "node -e \"var r=process.env.CLAUDE_PLUGIN_ROOT;if(r){var f=require('fs'),p=require('path');var t=f.readFileSync(p.join(r,'hooks','system-prompt.md'),'utf8');process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:t}}))}\"", + "command": "node -e \"var r=process.env.CLAUDE_PLUGIN_ROOT;if(r){require(require('path').join(r,'hooks','print-prompt.js'))}\"", "bash": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/print-prompt.js\"", "powershell": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/print-prompt.js\"" }, diff --git a/hooks/print-prompt.js b/hooks/print-prompt.js index 9b46674..e65d3b1 100644 --- a/hooks/print-prompt.js +++ b/hooks/print-prompt.js @@ -1,6 +1,50 @@ const path = require('path'); const fs = require('fs'); +// Only inject the Copilot Studio system prompt when this session is actually a +// Copilot Studio project — i.e. an `agent.mcs.yml` exists in the working +// directory or a subdirectory. This is the plugin's own definition of a CS +// project (see system-prompt.md: "already inside a Copilot Studio project ... +// there is an agent.mcs.yml file in the current directory or any subdirectory"). +// +// A SessionStart hook runs before any user message, so it cannot read user +// intent; the project marker is the only reliable signal. Without this gate the +// hook fires on every session and injects the ~11KB prompt into unrelated work. +function hasCopilotStudioProject(root, maxDepth, maxDirs) { + const skip = new Set([ + 'node_modules', '.git', '.svn', '.hg', 'dist', 'build', 'out', + '.next', '.cache', '.turbo', 'coverage', 'vendor', 'target', + ]); + const stack = [[root, 0]]; + let visited = 0; + while (stack.length) { + const [dir, depth] = stack.pop(); + if (++visited > maxDirs) return false; // bounded — never a long scan + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + continue; + } + for (const ent of entries) { + if (ent.isFile() && ent.name === 'agent.mcs.yml') return true; + if ( + ent.isDirectory() && + depth < maxDepth && + !skip.has(ent.name) && + !ent.name.startsWith('.') + ) { + stack.push([path.join(dir, ent.name), depth + 1]); + } + } + } + return false; +} + +if (!hasCopilotStudioProject(process.cwd(), 4, 2000)) { + process.exit(0); // not a Copilot Studio session — inject nothing +} + const text = fs.readFileSync(path.join(__dirname, 'system-prompt.md'), 'utf8'); // Copilot CLI (v1.0.11+) injects additionalContext from JSON output.