In
the previous post, you saw how a skill uses a script that detects the environment and returns only the instructions that apply. Instead of cramming every version of super-ops into one bloated SKILL.md, a thin skill calls a wrapper script, and the script creates the right guidance. The model incorporates that output and acts accordingly. Clean, focused, effective.
But there’s a catch. That entire mechanism depends on the model choosing to load the skill. A skill fires when the model reads its description and judges it relevant to the current conversation. Most of the time that works, but skills are non-deterministic. The model decides whether to use them based on how well the description matches the user’s request. If you ask about super-ops directly, the skill probably loads. If you ask a broader question that happens to involve super-ops indirectly, the model might skip it entirely. Your carefully crafted version-specific instructions never enter the context window (the text the model can read at one time), and the model falls back to its general training data.
When you need to guarantee that context is always present, you need a different mechanism. That’s where hooks come in.
The basics of AI hooks
A hook is a shell command that executes at a specific point in the Copilot agent’s lifecycle. Unlike a skill, which the model chooses to read, a hook fires unconditionally when its lifecycle event occurs. When the event triggers, your code runs. Period. No model judgment, no description matching, no discretion.
Hooks are available in VS Code, Copilot CLI, and Copilot cloud agent. The configuration format is similar across all three, with some naming and behavioral differences. Despite that, they mostly understand each other’s formats, making it easier to share scripts across environments.
Each hook receives structured JSON on stdin (standard input – the data piped into the script) describing the event, and it can write JSON to stdout (standard output – what the script prints) to influence the agent’s behavior – including injecting text directly into the model’s context, rewriting the prompt, or blocking specific requests. This is different from skills. These load on demand based on intent or request. They are not guaranteed to run, and they do not guarantee how the model will choose to use them. Hooks offer strong guarantees. They run when their event fires, and their output is deterministic. For example, if your hook returns context, that context appears in the model’s input every time. Both skills and hooks can shape the model’s behavior. The difference is who decides when this happens and defines the outcome. With skills, the model decides what to do and when. With hooks, your code is always executed at defined times.
The events that inject context
Hooks fire at specific lifecycle points. Not all of them can inject text into the conversation – some are purely for auditing, controlling permissions, or blocking operations. For this example, you’re specifically interested in events that can shape the model’s context.
VS Code’s hooks inject context via an additionalContext field in the returned JSON. For most events, that field must be nested inside a hookSpecificOutput wrapper that also names the event. That text is added immediately before the user’s prompt (and surrounded by <context> tags). This ensures the model reads your guidance before it interprets the request.
There are several events that can inject context using additionalContext, such as:
SessionStart/sessionStart– fires once when a new session beginsUserPromptSubmit/userPromptSubmitted– fires every time the user sends a promptPostToolUse/postToolUse– fires after a tool completes successfullySubagentStart/subagentStart– fires when a subagent is spawned
The naming difference reflects a compatibility detail: VS Code prefers PascalCase event names (like SessionStart) with snake_case payloads, while Copilot CLI prefers camelCase (like sessionStart), but can also use PascalCase/snake_case. The
hooks reference documents both formats side by side. Generally speaking, you must stick with one style or the other consistently. I will use PascalCase in this post because it is the same format preferred by other AI tools.
For deterministic context injection, SessionStart and UserPromptSubmit are two very useful events. These are “prompt hooks”. The first gives you a single injection point when the conversation begins – ideal for environment facts that won’t change mid-session. The SessionStart hook fires in Copilot CLI at the beginning of new or resumed interactive sessions (except in the cloud agent, which doesn’t fire for resume). UserPromptSubmit is fired at most once in response to the user submitting a prompt.
UserPromptSubmit is a good fit if you want to ensure that the instruction is added every time you submit a prompt, not just at the start of the session. You’ll start there so that you can see a small inconsistency between VS Code and Copilot CLI, then you’ll see SessionStart as an alternative. This hook has an important difference when used with Copilot CLI. It honors a different top-level field – modifiedPrompt. Instead of injecting content with additionalContext, it expects to receive modifiedPrompt containing the final version of the prompt the model will receive. It’s not adding content – it’s providing the entire modified prompt.
Solving super-ops with a hook
Let’s revisit the super-ops example from the previous post. As a reminder of the problem – you have a CLI tool with multiple versions and license tiers, and you need Copilot to receive the right instructions for the specific version/tier that is installed. Last time, you solved this with a skill that used a script to generate context. This time, you’ll solve the same problem using just a UserPromptSubmit hook.
First, create the hook configuration in .github/hooks/ as a JSON file that looks like this:
Save this as .github/hooks/super-ops.json. VS Code will automatically discover and load it the next time you invoke Copilot.
Next, create the script (scripts/super-ops-hook.sh in the root of your workspace). It reads the user’s prompt from stdin, detects the installed version/SKU of super-ops, and returns the matching instructions via additionalContext. The script uses jq, a command-line JSON processor (install it with apt install jq or brew install jq), to parse the event data and build the JSON response:
1#!/bin/bash
2
3### Read the prompt from stdin
4INPUT=$(cat)
5PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty')
6
7### Detect the installed version and license -- just like in the skill
8VERSION=$(super-ops --version 2>/dev/null | grep -oP '\d+')
9SKU=$(super-ops license status --format=short 2>/dev/null)
10
11if [[ -z "$VERSION" || -z "$SKU" ]]; then
12 echo '{}'
13 exit 0
14fi
15
16### Build the version-specific guidance
17case "${VERSION}-${SKU}" in
18 2-free) GUIDANCE="Configuration uses JSON format (config.json). Plugin commands are not available. Maximum 3 concurrent processes -- use super-ops run --limit=3." ;;
19 2-pro) GUIDANCE="Configuration uses JSON format (config.json). Plugins are installed with super-ops plugin add <name>. No concurrency limits." ;;
20 3-free) GUIDANCE="Configuration uses YAML format (config.yml). Plugin commands are not available. Maximum 5 concurrent processes -- use super-ops run --limit=5. Remote execution is not available." ;;
21 3-pro) GUIDANCE="Configuration uses YAML format (config.yml). Plugins are installed with super-ops plugin add <name>. No concurrency limits. Remote execution uses super-ops remote exec." ;;
22 *) GUIDANCE="Unknown super-ops version ${VERSION}-${SKU}. Ask the user to verify their installation." ;;
23esac
24
25### Create a JSON response that injects the guidance into additionalContext
26jq -n --arg ctx "$GUIDANCE" '{
27 additionalContext: $ctx
28}'With this hook in place, the script will execute every time you send a prompt. The script detects the version and license, then injects only the relevant instructions into the model’s context via additionalContext. As you start to work with hooks, pay attention to what is expected for a response: for UserPromptSubmit, VS Code expects additionalContext as a top-level property. Many hooks expect the response to have a hookSpecificOutput wrapper or hookEventName to set. This hook is much simpler. If you return the wrong fields, the results will be ignored.
Compare this to the skill-based approach from the previous post. The skill relied on the model reading the skill description, judging it relevant, loading the skill, following the instruction to run the script, and then treating the output as authoritative. That’s a chain of decisions the model makes – any link can break. The hook version skips all of that. The lifecycle event fires, your script runs, and the context is updated. The model never has to decide whether to load anything.
The same hook for Copilot CLI
Copilot CLI understands the very same hook – the same config file, the same script, and the same lifecycle event. You don’t need a separate hook JSON or a different mechanism. The .github/hooks/super-ops.json you already created works as-is; point the CLI at it and the hook fires on prompt submission just like it does in VS Code.
The one thing that genuinely differs is the response field each tool honors. VS Code reads additionalContext and adds that to the prompt. Copilot CLI reads modifiedPrompt, rewriting the prompt the model receives. Thankfully, each tool ignores fields it doesn’t understand. That makes supporting both tools trivial – just return both fields from one script.
Replace the single-field jq output from earlier with this, and each tool picks up the field it understands while ignoring the other:
VS Code reads additionalContext and surfaces it as <context> before your prompt; Copilot CLI ignores that field and applies modifiedPrompt, which is configured to do the same thing. One hook, one script, both tools – no branching on which agent is running. The trade-off with modifiedPrompt is that it incorporates your guidance into the user’s message rather than injecting it as separate field, but generally the end result is the same: the model receives the context and can act on it.
Using SessionStart
If you want to ensure the guidance is just sent once at the beginning of the session, there’s an alternative: move the injection to SessionStart. This hook diverges more between VS Code and Copilot CLI. VS Code expects the response to use a hookSpecificOutput wrapper with a hookEventName field identifying the event, like this:
Copilot CLI, by contrast, expects a flat response with just additionalContext at the top level – no wrapper, no event name:
The hook also has slightly different input payloads between the two tools. Copilot CLI always provides an initial_prompt field that contains the user’s prompt, while VS Code does not expose that value in this hook. VS Code, however, provides a transcript_path field that provides you with a JSONL (JSON Lines) file containing the full conversation history, including the user’s initial prompt. For super-ops version detection you’re not reading anything from the prompt, just detecting the installed version. As a result these differences don’t affect us.
Like before, you begin by declaring the hook and configuring it to run your script:
To support both tools from a single script, return both formats and let each tool pick up what it understands:
VS Code reads the nested hookSpecificOutput block; Copilot CLI reads just the top-level additionalContext. Each ignores the field it doesn’t recognize, so once again, a single script covers both. If you are building for just one tool, you can simplify the script to return only the expected format.
When to use hooks versus skills
Hooks and skills are complementary, not competing. The right choice depends on whether you need a guarantee or flexibility.
Use a hook when the context must appear unconditionally – environment facts, compliance rules, security guardrails, or anything that doesn’t require reasoning from the model. Hooks are deterministic: they fire on every matching lifecycle event, regardless of what the user asked or what the model is thinking.
Use a skill when you need to provide context based on reasoning or when the user directly invokes the skill. Skills are efficient precisely because they’re selective – the model loads them only when needed, keeping the context window lean for unrelated conversations. At the same time, skills are not guaranteed to load, even if you explicitly do something that seems like it should trigger them. For example, if your skill provides a way to search a particular file efficiently, the model could decide that it already knows enough about searching files from its training data and ignore the skill.
The most effective setups often combine both features depending on the use case.
Performance and trust
Because hooks execute synchronously and block the agent session, keep them fast. The documentation recommends targeting under five seconds of execution time. If your hook calls an external API or does heavy computation, consider caching results or moving the work to a background process that writes to a file the hook can read. You might even use a hook to start the heavy lifting asynchronously, then later retrieve the results.
Trust is the other consideration, and the supply-chain concerns from the previous post – where untrusted code in a repository you clone could manipulate your AI tooling – apply even more directly here. A skill requires the model to choose to load it – that’s one layer of indirection. A hook runs automatically just because a JSON file exists in .github/hooks/. That’s powerful for team standardization, but it means a malicious hook in a cloned repository injects harmful instructions into the model’s context without any model decision or user action beyond opening the project. Review hook scripts with the same scrutiny you’d apply to GitHub Actions workflows or postinstall scripts in package.json.
Wrapping up
The previous post showed how to make Copilot’s context smarter by generating instructions at runtime through a skill. This post solves the same problem from the other direction: instead of relying on the model to choose the right skill, you use a hook to guarantee the right context lands every time.
The mechanics are simple. You define a hook that fires on a lifecycle event. Your script detects the environment, builds the relevant guidance, and returns it as additionalContext. The model receives it as if it were built into the system prompt – no hoping the skill gets loaded, no chain of model decisions to break. Your code runs, your context appears, every time.
