New May 26, 2026

Creating a VS Code agent hook to respond to file changes

Top Front-end Bloggers All from Human Who Codes View Creating a VS Code agent hook to respond to file changes on humanwhocodes.com

Visual Studio Code recently shipped [agent hooks]1 as a way to customize your agent experience. Unlike other enhancements such as skills, agent hooks are deterministic. Instead of relying on the agent to call a tool or use a skill, an agent hook sits in the middle of the agent loop and listens for specific events. You can use those events to trigger an action. That means no more pleading with the agent to always follow one action with another. Just define a hook that makes it happen 100% of the time.

For example, I recently set up an agent hook to regenerate TypeScript types whenever a wrangler.jsonc file is edited. These files specify bindings for Cloudflare Workers, and the wrangler types command reads wrangler.jsonc and outputs TypeScript type definitions. I had been struggling to get agents to regenerate types whenever they edited a wrangler.jsonc file. I begged, pleaded, and strongly cautioned the agent, and nothing worked. Now it happens automatically with an agent hook, and I don’t have to think about it.

Creating an agent hook that triggers only when a specific file changes isn’t straightforward. It takes a fair amount of boilerplate just to determine whether the file in question changed. Before digging into that use case, though, it helps to understand how agent hooks work in general.

How agent hooks work

Agent hooks are shell commands that execute at specific points during an agent session. Every agent session consists of a series of events, and you can trigger an agent hook for any of them. At the time of writing, the available hook events are:

You can hook into any of these events by creating a JSON file in the .github/hooks directory that specifies which events to listen for and which command to run. Here’s an example:

{
  "hooks": {
    "PostToolUse": [
      {
        "type": "command",
        "command": "bash ./hello-world.sh"
      }
    ]
  }
}

This hook runs the hello-world.sh script with bash after a tool has been used.

Hook input

When a hook command is executed, details about the hook are passed through stdin as a JSON structure. Five common fields are sent with every hook:

Each hook event adds hook-specific information to the payload. For example, a PostToolUse hook, which is the one this post focuses on, adds information about the tool that was used:

{
  "timestamp": "2026-02-09T10:30:00.000Z",
  "cwd": "/path/to/workspace",
  "sessionId": "session-identifier",
  "hookEventName": "PostToolUse",
  "transcript_path": "/path/to/transcript.json",
  "tool_name": "editFiles",
  "tool_input": { "files": ["src/main.ts"] },
  "tool_use_id": "tool-123",
  "tool_response": "File edited successfully"
}

Hook output

Your hook command communicates back to VS Code through two mechanisms:

  1. The exit code
  2. stdout

The exit code can be one of the following:

The exit code is used along with a JSON structure written to stdout. Every hook event supports the following fields:

For PostToolUse, you can also specify these fields:

Here’s an example:

{
  "continue": true,
  "stopReason": "Security policy violation",
  "systemMessage": "Unit tests failed",
  "decision": "block",
  "reason": "Post-processing validation failed",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "The edited file has lint errors that need to be fixed"
  }
}

Detecting when a specific file is edited

While there is no direct way to determine whether a given file was edited, you can infer it by checking when known editing tools were executed against that file.

Step 1: Read from stdin

The first step is to read data from stdin. To do that, it helps to create a wrapper function:

function readStdin() {
  return new Promise((resolve, reject) => {
    let data = "";

    process.stdin.setEncoding("utf8");
    process.stdin.on("data", chunk => {
      data += chunk;
    });
    process.stdin.on("end", () => resolve(data));
    process.stdin.on("error", reject);
  });
}

Then you can read the data and parse it into an object:

const raw = await readStdin();
const payload = JSON.parse(raw);

Step 2: Detect changes to the file

Now that you have the hook payload, you need to filter for the tools that can create or edit files. VS Code has several of these tools, so the first step is to define them.

const WRITE_TOOL_NAMES = new Set([
  "apply_patch",
  "create_file",
  "replace_string_in_file",
  "editFiles",
  "createFile",
  "edit",
  "create",
  "write",
  "str_replace_editor",
  "str_replace_based_edit_tool",
]);

Then you can filter the hook payload by tool_name to ensure you aren’t responding to unrelated tool calls:

// if this isn't a tool we're looking for then just exit
if (!WRITE_TOOL_NAMES.has(payload.tool_name)) {
  process.stdout.write(JSON.stringify({ continue: true }));
  process.exit(0);
}

After that, you can check whether the target filename is present in the tool_input.files array. Keep in mind that the file paths are relative to the current working directory, so if you want to calculate the directory containing the file, you’ll need to resolve it first. Here’s an example that looks for wrangler.jsonc and extracts the parent directory:

const relativePath = payload.tool_input.files.find(
  filePath => path.basename(filePath) === "wrangler.jsonc"
);

// file wasn't found so just exit
if (!relativePath) {{
  process.stdout.write(JSON.stringify({ continue: true }));
  process.exit(0);  
}}

const parentDirectory = path.dirname(path.resolve(payload.cwd, relativePath));

If this code executes all the way through, that means a file named wrangler.jsonc was edited, and you can run any additional code related to that change. Just be sure to exit gracefully once you’re done:

// do what you want with relativePath and parentDirectory

// exit gracefully at the end of the script
process.stdout.write(JSON.stringify({ continue: true }));
process.exit(0);  

Step 3: Wire up the hook

The last step is to wire up the hook. By convention, hook scripts are stored in .github/hooks/scripts, so you can create a JSON file like this in .github/hooks:

{
  "hooks": {
    "PostToolUse": [
      {
        "type": "command",
        "command": "node ./scripts/detect-file-change.mjs"
      }
    ]
  }
}

Now, whenever any file named wrangler.jsonc is edited, the hook command will execute.

Conclusion

Agent hooks add a new layer of reliability to customizing the agent experience in VS Code. Instead of hoping the model remembers to perform a follow-up task, you can enforce that behavior with a deterministic command. It takes some setup, especially when you need to detect changes to a specific file, but the payoff is worth it. Once the hook is in place, the workflow becomes automatic, consistent, and one less thing to think about.

Footnotes

  1. Agent hooks in Visual Studio Code ↩

Scroll to top