
I tried setting up guardrails with hooks to prevent accidentally registering issues to the wrong project via the Backlog MCP Server
This page has been translated by machine translation. View original
This is Iwata from the Retail App Co-Creation Department @ Osaka.
Backlog MCP Server is a very convenient tool for operating Backlog from AI agents like Claude Code.
However, just because it's convenient doesn't mean you should use it without setting up appropriate guardrails — there's a risk of accidents caused by unintended AI behavior. In this blog post, I'll introduce an implementation sample that uses hooks as guardrails when using Backlog MCP Server, restricting operations to specific Backlog projects.
※ Going forward, the word "project" refers to the directory where Claude Code is being executed. It is used in the context of user-level settings/project-level settings, and does not refer to Claude's projects. Also, Backlog projects will be explicitly referred to as "Backlog projects."
Introduction
If there are multiple Backlog projects within a Backlog space connected via Backlog MCP Server, there is a concern that you might accidentally register an issue to Backlog project B when you intended to register it to Backlog project A. Even if you write something like "When using Backlog MCP Server, always specify Backlog project ID 12345!!!" in Claude.md, there's no guarantee it will be followed. To reliably prevent unintended behavior, deterministic protection through hooks is effective.
Let's Try It
With that, let's go ahead and register the hooks.
Thinking About the Design
First, I thought about which scope the hooks should be set at. Setting them at the project level raises concerns about missing hook configurations for newly added projects in the future, so it seems better to add them as user-level settings.
However, making them user-level settings means project-specific configurations can't be written, which requires the hooks to be generic. You can't hardcode Backlog project IDs directly into the shell script. On the other hand, since the Backlog projects whose issues you want to allow operations on should differ per project, I decided to list the allowed Backlog projects in a text file within the project, and have the script read this text file to determine whether a tool call is permitted.
For this implementation, I'll list the IDs of Backlog projects allowed for operations in a text file called backlog.allow.
Registering the PreToolUse Hook
First, add the hooks description to ~/.claude/settings.json as follows.
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__backlog__add_issue",
"hooks": [
{
"type": "command",
"command": "${HOME}/.claude/hooks/backlog/project-check.sh || { echo 'hook script execution failed' 1>&2; exit 2; }"
}
]
}
]
}
mcp__backlog__add_issue is specified for matcher. This assumes Backlog MCP Server is registered under the name backlog, and with this configuration, the hooks will fire when the add_issue tool of Backlog MCP Server is called.
project-check.sh is specified as a shell script for command. If this shell script doesn't exist, || { echo 'hook script execution failed' 1>&2; exit 2; is specified so that it exits with completion code 2 and the tool call is blocked.
Implementing the Shell Script
Next, let's implement project-check.sh specified in the hooks earlier. Here's what the content looks like.
#!/bin/bash
PROJECT_ID=$(jq -r '.tool_input.projectId')
ALLOW_FILE="${CLAUDE_PROJECT_DIR}/.backlog.allow"
if [ ! -f "$ALLOW_FILE" ]; then
jq -n "{
hookSpecificOutput: {
hookEventName: \"PreToolUse\",
permissionDecision: \"deny\",
permissionDecisionReason: \"Allow file not found: $ALLOW_FILE\"
}
}"
exit 2
fi
if ! grep -Fxq "$PROJECT_ID" "$ALLOW_FILE"; then
jq -n "{
hookSpecificOutput: {
hookEventName: \"PreToolUse\",
permissionDecision: \"deny\",
permissionDecisionReason: \"Project ID $PROJECT_ID not allowed\"
}
}"
exit 2
fi
exit 0
First, PROJECT_ID=$(jq -r '.tool_input.projectId') retrieves the Backlog project ID from the tool call arguments.
In the subsequent processing, it checks whether a .backlog.allow file exists under the project directory, and if it doesn't exist, the tool call is denied. If the file exists, it checks whether the Backlog project ID specified in the tool arguments exists within .backlog.allow and determines whether the tool call is permitted.
Testing It Out
Now that the hooks are ready, let's test them. All tests below were conducted using the following prompt and sonnet5.
Please register an issue using the mcp__backlog__add_issue tool
Project ID is 826921
Title is "test from claude code", issueTypeId is 4436305, priorityId is 2
First, the case where I forgot to grant execute permissions to the shell script...

It resulted in an error saying Issue registration is blocked because the hook script ~/.claude/hooks/backlog/project-check.sh does not have execute permission. and did not proceed with registering the issue.
Next, the result when the .backlog.allow text file does not exist.

This also resulted in an error as expected.
Next, the result when the Backlog project ID specified in the prompt does not exist within .backlog.allow.

Execution failed with Issue registration failed because access to project ID 826921 is not permitted ("Project ID 826921 not allowed" error).
Finally, the result when the Backlog project ID specified in the prompt exists within .backlog.allow.

Registration succeeded with the message Issue created. Registered to project ID 826921 as TEST-9 ("test from claude code", type: Task, priority: High). The issue was properly registered.

Summary
This time I only tried add_issue for now, but for delete_issue and update_issue as well, by implementing hooks that deny tool calls by issueId while checking the issueKey prefix, it seems possible to implement guardrails that allow issue operations only for specific Backlog projects.
※ I regretted that the design of listing Backlog project IDs in a text file has poor extensibility — I should have stored the configuration in JSON format.
Another issue is that if the MCP server is not registered under the name backlog, the tool name won't become mcp__backlog__add_issue, and it won't match the hooks matcher...
