本教程适用于支持开发人员使用 Copilot 命令行界面(CLI) 的 DevOps 工程师、平台团队以及工程负责人。
Hooks 是在 Copilot 命令行界面(CLI) 会话期间特定时间点运行的自定义脚本。 他们可以检查提示和工具调用、记录审核信息,甚至阻止执行某些命令。
你将配置一些作用于存储库范围的钩子:
- 提供对提示和工具使用情况的可见性。
- 在执行前阻止高风险命令模式。
- 帮助开发人员了解具有明确消息传送的组织策略。
先决条件
- 熟悉 shell 脚本(Bash 或 PowerShell)
- 基本了解 JSON 配置文件
- 可访问正在使用 Copilot 命令行界面(CLI) 的仓库
-
`jq` 已安装 (对于 Bash 示例)
1. 定义组织策略
在编写任何挂钩脚本之前,请确定哪些操作应自动允许,哪些操作需要人工审核。
明确的策略有助于避免过度阻塞,同时仍降低风险。
确定始终需要审阅的命令
首先识别那些绝不应由 Copilot 命令行界面(CLI) 自动执行的模式。 常见示例包括:
-
**权限提升**: `sudo`, , `su``runas` -
**破坏性系统操作**:`rm -rf /`、`mkfs`、`dd`、`format` -
**下载和执行模式**:`curl ... | bash`、PowerShell `wget ... | sh``iex (irm ...)`
这些命令在无意中执行时可能会产生不可逆的影响。
确定要记录的内容
使用 hooks 时,你可以捕获 Copilot 命令行界面(CLI) 在仓库中的使用情况信息,包括用户提交的提示以及 Copilot 命令行界面(CLI) 尝试运行的工具。
大多数组织至少记录:
- 时间戳和存储库路径
- 提示文本(或经过编辑的形式)
- 工具名称和工具参数
- 任何策略决策(例如,被拒绝的命令及其原因)
请勿在日志中记录机密信息或凭证。 如果提示或命令可能包含敏感数据,请在编写日志之前应用修订。
本教程使用本地 .github/hooks/logs 目录作为一个简单的演示示例。 这些日志文件 不打算提交到存储库 ,通常仅存在于开发人员的计算机上。
在生产环境中,许多组织将事件转发到集中式日志记录或可观测性系统,而不是在本地编写日志。 这样,团队就可以在存储库和用户之间应用一致的编辑、访问控制、保留策略和监控。
与利益干系人保持一致
在强制实施策略之前,与以下人员一同审核它们:
- 安全或合规性团队,以确认风险边界
- 平台或基础结构团队,他们可能需要更广泛的权限
- 开发团队,因此他们了解将被阻止的内容以及原因
明确的预期使政策强制实施更容易采用和维护。
2.设置存储库挂钩文件
在整个教程中,你将使用存储在仓库中 .github/hooks/ 下的仓库级 hooks。 每当 数据变量.copilot.copilot_cli_short %} 从该存储库内运行时,这些钩子适用。
注意
Copilot 代理从存储库中的 .github/hooks/*.json 中加载挂钩配置文件。 钩子同步运行,可以阻止程序的执行。
创建目录结构
在存储库根目录中,为挂钩配置、脚本和日志创建目录:
mkdir -p .github/hooks/scripts mkdir -p .github/hooks/logs
mkdir -p .github/hooks/scripts
mkdir -p .github/hooks/logs
将 .github/hooks/logs/ 添加到 .gitignore 中,以避免本地审计日志被提交:
echo ".github/hooks/logs/" >> .gitignore
echo ".github/hooks/logs/" >> .gitignore
本教程使用以下结构:
.github/
└── hooks/
├── copilot-cli-policy.json
├── logs/
│ └── audit.jsonl
└── scripts/
├── session-banner.sh
├── session-banner.ps1
├── log-prompt.sh
├── log-prompt.ps1
├── pre-tool-policy.sh
└── pre-tool-policy.ps1
创建挂钩配置文件
在 .github/hooks/copilot-cli-policy.json 创建挂钩配置文件。
此文件定义哪些挂钩、何时运行以及执行哪些脚本。
{
"version": 1,
"hooks": {
"sessionStart": [
{
"type": "command",
"bash": "./scripts/session-banner.sh",
"powershell": "./scripts/session-banner.ps1",
"cwd": ".github/hooks",
"timeoutSec": 10
}
],
"userPromptSubmitted": [
{
"type": "command",
"bash": "./scripts/log-prompt.sh",
"powershell": "./scripts/log-prompt.ps1",
"cwd": ".github/hooks",
"timeoutSec": 10
}
],
"preToolUse": [
{
"type": "command",
"bash": "./scripts/pre-tool-policy.sh",
"powershell": "./scripts/pre-tool-policy.ps1",
"cwd": ".github/hooks",
"timeoutSec": 15
}
]
}
}
{
"version": 1,
"hooks": {
"sessionStart": [
{
"type": "command",
"bash": "./scripts/session-banner.sh",
"powershell": "./scripts/session-banner.ps1",
"cwd": ".github/hooks",
"timeoutSec": 10
}
],
"userPromptSubmitted": [
{
"type": "command",
"bash": "./scripts/log-prompt.sh",
"powershell": "./scripts/log-prompt.ps1",
"cwd": ".github/hooks",
"timeoutSec": 10
}
],
"preToolUse": [
{
"type": "command",
"bash": "./scripts/pre-tool-policy.sh",
"powershell": "./scripts/pre-tool-policy.ps1",
"cwd": ".github/hooks",
"timeoutSec": 15
}
]
}
}
了解此配置的作用
此配置设置三个挂钩:
-
`sessionStart`:显示新代理会话启动或恢复时的信息性消息。 -
`userPromptSubmitted`:每当用户提交提示时运行。 -
`preToolUse`:在工具执行之前运行,并且可以显式允许或拒绝执行。
提交并共享挂钩配置
当您准备好与协作者共享钩子配置时(例如,通过拉取请求或在测试存储库中),请提交钩子配置和脚本。 不要提交任何本地审核日志。
git add .github/hooks/copilot-cli-policy.json .github/hooks/scripts git commit -m "Add Copilot CLI hook configuration" git push
git add .github/hooks/copilot-cli-policy.json .github/hooks/scripts
git commit -m "Add Copilot CLI hook configuration"
git push
此时,即使你尚未创建 hook 脚本,Copilot 命令行界面(CLI) 也已经能够发现你的 hook 配置。
3. 在会话开始时添加策略横幅
使用一个 sessionStart hook,在每次新的 Copilot 命令行界面(CLI) 会话开始或恢复时显示横幅信息。 这向开发人员明确表示组织策略处于活动状态。
挂钩函数 sessionStart 接收上下文相关信息,例如当前工作目录和初始提示。 该 hook 的任何输出都会被 Copilot 命令行界面(CLI) 忽略,因此非常适合用于信息性提示。
创建会话横幅脚本 (Bash)
创建 .github/hooks/scripts/session-banner.sh:
#!/bin/bash set -euo pipefail cat << 'EOF' COPILOT CLI POLICY ACTIVE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Prompts and tool use may be logged for auditing • High-risk commands may be blocked automatically • If something is blocked, follow the guidance shown ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ EOF exit 0
#!/bin/bash
set -euo pipefail
cat << 'EOF'
COPILOT CLI POLICY ACTIVE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• Prompts and tool use may be logged for auditing
• High-risk commands may be blocked automatically
• If something is blocked, follow the guidance shown
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
exit 0
创建会话横幅脚本 (PowerShell)
创建 .github/hooks/scripts/session-banner.ps1:
$ErrorActionPreference = "Stop" Write-Host @" COPILOT CLI POLICY ACTIVE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Prompts and tool use may be logged for auditing • High-risk commands may be blocked automatically • If something is blocked, follow the guidance shown ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ "@ exit 0
$ErrorActionPreference = "Stop"
Write-Host @"
COPILOT CLI POLICY ACTIVE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• Prompts and tool use may be logged for auditing
• High-risk commands may be blocked automatically
• If something is blocked, follow the guidance shown
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"@
exit 0
测试会话横幅
可以直接测试横幅脚本:
.github/hooks/scripts/session-banner.sh
# or, for PowerShell
.github/hooks/scripts/session-banner.ps1
运行任一脚本时,应会看到终端中显示的策略横幅。
4.记录提示以用于审计
请使用 userPromptSubmitted 挂钩记录用户何时向 Copilot 命令行界面(CLI) 提交提示。 每次发送提示时,都会先运行此钩子,然后再调用任何工具。
挂钩接收结构化 JSON 输入,其中包括时间戳、当前工作目录和完整提示文本。 忽略此钩子的输出。
重要
提示可能包含敏感信息。 在记录此数据时应用修订并遵循组织的数据处理和保留策略。
创建命令提示符日志记录脚本 (Bash)
创建 .github/hooks/scripts/log-prompt.sh:
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
TIMESTAMP_MS="$(echo "$INPUT" | jq -r '.timestamp // empty')"
CWD="$(echo "$INPUT" | jq -r '.cwd // empty')"
# This example logs only metadata, not the full prompt, to avoid storing
# potentially sensitive data. Adjust to match your organization’s needs.
LOG_DIR=".github/hooks/logs"
mkdir -p "$LOG_DIR"
chmod 700 "$LOG_DIR"
jq -n \
--arg ts "$TIMESTAMP_MS" \
--arg cwd "$CWD" \
'{event:"userPromptSubmitted", timestampMs:$ts, cwd:$cwd}' \
>> "$LOG_DIR/audit.jsonl"
exit 0
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
TIMESTAMP_MS="$(echo "$INPUT" | jq -r '.timestamp // empty')"
CWD="$(echo "$INPUT" | jq -r '.cwd // empty')"
# This example logs only metadata, not the full prompt, to avoid storing
# potentially sensitive data. Adjust to match your organization’s needs.
LOG_DIR=".github/hooks/logs"
mkdir -p "$LOG_DIR"
chmod 700 "$LOG_DIR"
jq -n \
--arg ts "$TIMESTAMP_MS" \
--arg cwd "$CWD" \
'{event:"userPromptSubmitted", timestampMs:$ts, cwd:$cwd}' \
>> "$LOG_DIR/audit.jsonl"
exit 0
创建提示日志记录脚本 (PowerShell)
创建 .github/hooks/scripts/log-prompt.ps1:
$ErrorActionPreference = "Stop"
$inputObj = [Console]::In.ReadToEnd() | ConvertFrom-Json
$timestampMs = $inputObj.timestamp
$cwd = $inputObj.cwd
$prompt = $inputObj.prompt
# Optional example redaction. Adjust to match your organization’s needs.
$redactedPrompt = $prompt -replace 'ghp_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]'
$logDir = ".github/hooks/logs"
if (-not (Test-Path $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
}
$logEntry = @{
event = "userPromptSubmitted"
timestampMs = $timestampMs
cwd = $cwd
prompt = $redactedPrompt
} | ConvertTo-Json -Compress
Add-Content -Path "$logDir/audit.jsonl" -Value $logEntry
exit 0
$ErrorActionPreference = "Stop"
$inputObj = [Console]::In.ReadToEnd() | ConvertFrom-Json
$timestampMs = $inputObj.timestamp
$cwd = $inputObj.cwd
$prompt = $inputObj.prompt
# Optional example redaction. Adjust to match your organization’s needs.
$redactedPrompt = $prompt -replace 'ghp_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]'
$logDir = ".github/hooks/logs"
if (-not (Test-Path $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
}
$logEntry = @{
event = "userPromptSubmitted"
timestampMs = $timestampMs
cwd = $cwd
prompt = $redactedPrompt
} | ConvertTo-Json -Compress
Add-Content -Path "$logDir/audit.jsonl" -Value $logEntry
exit 0
测试提示日志记录脚本
你可以通过管道传入示例输入来直接测试脚本。
echo '{"timestamp":1704614500000,"cwd":"/repo","prompt":"List all branches"}' \
| .github/hooks/scripts/log-prompt.sh
# or, for PowerShell
echo '{"timestamp":1704614500000,"cwd":"/repo","prompt":"List all branches"}' |
.github/hooks/scripts/log-prompt.ps1
运行脚本后,检查.github/hooks/logs/audit.jsonl是否有新的日志条目。
cat .github/hooks/logs/audit.jsonl
cat .github/hooks/logs/audit.jsonl
此时,在该仓库中提交给 Copilot 命令行界面(CLI) 的提示都会被记录下来以供审计。
5.使用 preToolUse 强制执行策略
使用preToolUse挂钩来在工具调用运行之前进行评估。 此挂钩可以允许执行(不采取任何行动)或拒绝执行(通过返回结构化响应)。
了解 preToolUse 的输入
`preToolUse` hook 的输入包括:
*
toolNameCopilot 命令行界面(CLI) 即将运行的工具(例如 bash)
*
toolArgs:包含该工具参数的 JSON 字符串
因为 toolArgs 是 JSON 字符串,所以脚本在读取类似 command字段之前必须对其进行分析。
重要
工具参数和命令可能包含敏感信息,例如 API 令牌、密码或其他凭据。 在记录此数据之前应用修订,并遵循组织的安全策略。 请考虑仅记录非敏感元数据(工具名称、时间戳、策略决策),并将审核事件定向到具有适当access控制和保留策略的安全集中日志记录系统。
创建策略脚本
接下来,创建策略脚本。 本示例:
- 记录所有尝试的工具使用情况。
- 仅对 bash 命令应用拒绝规则。
- 阻止高风险模式,例如特权升级、破坏性操作和下载并执行命令。
为了安全地验证拒绝流,该脚本还包括一个临时演示规则,用于阻止无害的测试命令。 在确认 hooks 按预期工作后,移除演示规则,并将其替换为符合你组织策略的模式。
示例脚本 (Bash)
创建 .github/hooks/scripts/pre-tool-policy.sh:
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
TOOL_NAME="$(echo "$INPUT" | jq -r '.toolName // empty')"
TOOL_ARGS_RAW="$(echo "$INPUT" | jq -r '.toolArgs // empty')" # JSON string
LOG_DIR=".github/hooks/logs"
mkdir -p "$LOG_DIR"
# Example redaction logic.
# GitHub does not currently provide built-in secret redaction for hooks.
# This example shows one possible approach; many organizations prefer to
# forward events to a centralized logging system that handles redaction.
# Redact sensitive patterns before logging.
# Adjust these patterns to match your organization's needs.
REDACTED_TOOL_ARGS="$(echo "$TOOL_ARGS_RAW" | \
sed -E 's/ghp_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/gho_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/ghu_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/ghs_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/Bearer [A-Za-z0-9_\-\.]+/Bearer [REDACTED]/g' | \
sed -E 's/--password[= ][^ ]+/--password=[REDACTED]/g' | \
sed -E 's/--token[= ][^ ]+/--token=[REDACTED]/g')"
# Log attempted tool use with redacted toolArgs.
jq -n \
--arg tool "$TOOL_NAME" \
--arg toolArgs "$REDACTED_TOOL_ARGS" \
'{event:"preToolUse", toolName:$tool, toolArgs:$toolArgs}' \
>> "$LOG_DIR/audit.jsonl"
# Only enforce command rules for bash.
if [ "$TOOL_NAME" != "bash" ]; then
exit 0
fi
# Parse toolArgs JSON string.
# If toolArgs isn't valid JSON for some reason, allow (and rely on logs).
if ! echo "$TOOL_ARGS_RAW" | jq -e . >/dev/null 2>&1; then
exit 0
fi
COMMAND="$(echo "$TOOL_ARGS_RAW" | jq -r '.command // empty')"
# ---------------------------------------------------------------------------
# Demo-only deny rule for safe testing.
# This blocks a harmless test command so you can validate the deny flow.
# Remove this rule after confirming your hooks work as expected.
# ---------------------------------------------------------------------------
if echo "$COMMAND" | grep -q "COPILOT_HOOKS_DENY_DEMO"; then
deny "Blocked demo command (test rule). Remove this rule after validating hooks."
fi
deny() {
local reason="$1"
# Redact sensitive patterns from command before logging.
local redacted_cmd="$(echo "$COMMAND" | \
sed -E 's/ghp_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/gho_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/ghu_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/ghs_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/Bearer [A-Za-z0-9_\-\.]+/Bearer [REDACTED]/g' | \
sed -E 's/--password[= ][^ ]+/--password=[REDACTED]/g' | \
sed -E 's/--token[= ][^ ]+/--token=[REDACTED]/g')"
# Log the denial decision with redacted command.
jq -n \
--arg cmd "$redacted_cmd" \
--arg r "$reason" \
'{event:"policyDeny", toolName:"bash", command:$cmd, reason:$r}' \
>> "$LOG_DIR/audit.jsonl"
# Return a denial response.
jq -n \
--arg r "$reason" \
'{permissionDecision:"deny", permissionDecisionReason:$r}'
exit 0
}
# Privilege escalation
if echo "$COMMAND" | grep -qE '\b(sudo|su|runas)\b'; then
deny "Privilege escalation requires manual approval."
fi
# Destructive filesystem operations targeting root
if echo "$COMMAND" | grep -qE 'rm\s+-rf\s*/($|\s)|rm\s+.*-rf\s*/($|\s)'; then
deny "Destructive operations targeting the filesystem root require manual approval."
fi
# System-level destructive operations
if echo "$COMMAND" | grep -qE '\b(mkfs|dd|format)\b'; then
deny "System-level destructive operations are not allowed via automated execution."
fi
# Download-and-execute patterns
if echo "$COMMAND" | grep -qE 'curl.*\|\s*(bash|sh)|wget.*\|\s*(bash|sh)'; then
deny "Download-and-execute patterns require manual approval."
fi
# Allow by default
exit 0
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
TOOL_NAME="$(echo "$INPUT" | jq -r '.toolName // empty')"
TOOL_ARGS_RAW="$(echo "$INPUT" | jq -r '.toolArgs // empty')" # JSON string
LOG_DIR=".github/hooks/logs"
mkdir -p "$LOG_DIR"
# Example redaction logic.
# GitHub does not currently provide built-in secret redaction for hooks.
# This example shows one possible approach; many organizations prefer to
# forward events to a centralized logging system that handles redaction.
# Redact sensitive patterns before logging.
# Adjust these patterns to match your organization's needs.
REDACTED_TOOL_ARGS="$(echo "$TOOL_ARGS_RAW" | \
sed -E 's/ghp_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/gho_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/ghu_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/ghs_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/Bearer [A-Za-z0-9_\-\.]+/Bearer [REDACTED]/g' | \
sed -E 's/--password[= ][^ ]+/--password=[REDACTED]/g' | \
sed -E 's/--token[= ][^ ]+/--token=[REDACTED]/g')"
# Log attempted tool use with redacted toolArgs.
jq -n \
--arg tool "$TOOL_NAME" \
--arg toolArgs "$REDACTED_TOOL_ARGS" \
'{event:"preToolUse", toolName:$tool, toolArgs:$toolArgs}' \
>> "$LOG_DIR/audit.jsonl"
# Only enforce command rules for bash.
if [ "$TOOL_NAME" != "bash" ]; then
exit 0
fi
# Parse toolArgs JSON string.
# If toolArgs isn't valid JSON for some reason, allow (and rely on logs).
if ! echo "$TOOL_ARGS_RAW" | jq -e . >/dev/null 2>&1; then
exit 0
fi
COMMAND="$(echo "$TOOL_ARGS_RAW" | jq -r '.command // empty')"
# ---------------------------------------------------------------------------
# Demo-only deny rule for safe testing.
# This blocks a harmless test command so you can validate the deny flow.
# Remove this rule after confirming your hooks work as expected.
# ---------------------------------------------------------------------------
if echo "$COMMAND" | grep -q "COPILOT_HOOKS_DENY_DEMO"; then
deny "Blocked demo command (test rule). Remove this rule after validating hooks."
fi
deny() {
local reason="$1"
# Redact sensitive patterns from command before logging.
local redacted_cmd="$(echo "$COMMAND" | \
sed -E 's/ghp_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/gho_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/ghu_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/ghs_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
sed -E 's/Bearer [A-Za-z0-9_\-\.]+/Bearer [REDACTED]/g' | \
sed -E 's/--password[= ][^ ]+/--password=[REDACTED]/g' | \
sed -E 's/--token[= ][^ ]+/--token=[REDACTED]/g')"
# Log the denial decision with redacted command.
jq -n \
--arg cmd "$redacted_cmd" \
--arg r "$reason" \
'{event:"policyDeny", toolName:"bash", command:$cmd, reason:$r}' \
>> "$LOG_DIR/audit.jsonl"
# Return a denial response.
jq -n \
--arg r "$reason" \
'{permissionDecision:"deny", permissionDecisionReason:$r}'
exit 0
}
# Privilege escalation
if echo "$COMMAND" | grep -qE '\b(sudo|su|runas)\b'; then
deny "Privilege escalation requires manual approval."
fi
# Destructive filesystem operations targeting root
if echo "$COMMAND" | grep -qE 'rm\s+-rf\s*/($|\s)|rm\s+.*-rf\s*/($|\s)'; then
deny "Destructive operations targeting the filesystem root require manual approval."
fi
# System-level destructive operations
if echo "$COMMAND" | grep -qE '\b(mkfs|dd|format)\b'; then
deny "System-level destructive operations are not allowed via automated execution."
fi
# Download-and-execute patterns
if echo "$COMMAND" | grep -qE 'curl.*\|\s*(bash|sh)|wget.*\|\s*(bash|sh)'; then
deny "Download-and-execute patterns require manual approval."
fi
# Allow by default
exit 0
创建策略脚本 (PowerShell)
创建 .github/hooks/scripts/pre-tool-policy.ps1:
$ErrorActionPreference = "Stop"
$inputObj = [Console]::In.ReadToEnd() | ConvertFrom-Json
$toolName = $inputObj.toolName
$toolArgsRaw = $inputObj.toolArgs # JSON string
$logDir = ".github/hooks/logs"
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
# Example redaction logic.
# GitHub does not currently provide built-in secret redaction for hooks.
# This example shows one possible approach; many organizations prefer to
# forward events to a centralized logging system that handles redaction.
# Redact sensitive patterns before logging.
# Adjust these patterns to match your organization's needs.
$redactedToolArgs = $toolArgsRaw `
-replace 'ghp_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'gho_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'ghu_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'ghs_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'Bearer [A-Za-z0-9_\-\.]+', 'Bearer [REDACTED]' `
-replace '--password[= ][^ ]+', '--password=[REDACTED]' `
-replace '--token[= ][^ ]+', '--token=[REDACTED]'
# Log attempted tool use with redacted toolArgs.
(@{
event = "preToolUse"
toolName = $toolName
toolArgs = $redactedToolArgs
} | ConvertTo-Json -Compress) | Add-Content -Path "$logDir/audit.jsonl"
if ($toolName -ne "bash") { exit 0 }
# Parse toolArgs JSON string.
$toolArgs = $null
try { $toolArgs = $toolArgsRaw | ConvertFrom-Json } catch { exit 0 }
$command = $toolArgs.command
# ---------------------------------------------------------------------------
# Demo-only deny rule for safe testing.
# This blocks a harmless test command so you can validate the deny flow.
# Remove this rule after confirming your hooks work as expected.
# ---------------------------------------------------------------------------
if ($command -match 'COPILOT_HOOKS_DENY_DEMO') {
Deny "Blocked demo command (test rule). Remove this rule after validating hooks."
}
function Deny([string]$reason) {
# Redact sensitive patterns from command before logging.
$redactedCommand = $command `
-replace 'ghp_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'gho_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'ghu_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'ghs_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'Bearer [A-Za-z0-9_\-\.]+', 'Bearer [REDACTED]' `
-replace '--password[= ][^ ]+', '--password=[REDACTED]' `
-replace '--token[= ][^ ]+', '--token=[REDACTED]'
# Log the denial decision with redacted command.
(@{
event = "policyDeny"
toolName = "bash"
command = $redactedCommand
reason = $reason
} | ConvertTo-Json -Compress) | Add-Content -Path "$logDir/audit.jsonl"
(@{
permissionDecision = "deny"
permissionDecisionReason = $reason
} | ConvertTo-Json -Compress)
exit 0
}
if ($command -match '\b(sudo|su|runas)\b') { Deny "Privilege escalation requires manual approval." }
if ($command -match 'rm\s+-rf\s*/(\s|$)|rm\s+.*-rf\s*/(\s|$)') { Deny "Destructive operations targeting the filesystem root require manual approval." }
if ($command -match '\b(mkfs|dd|format)\b') { Deny "System-level destructive operations are not allowed via automated execution." }
if ($command -match 'curl.*\|\s*(bash|sh)|wget.*\|\s*(bash|sh)') { Deny "Download-and-execute patterns require manual approval." }
exit 0
$ErrorActionPreference = "Stop"
$inputObj = [Console]::In.ReadToEnd() | ConvertFrom-Json
$toolName = $inputObj.toolName
$toolArgsRaw = $inputObj.toolArgs # JSON string
$logDir = ".github/hooks/logs"
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
# Example redaction logic.
# GitHub does not currently provide built-in secret redaction for hooks.
# This example shows one possible approach; many organizations prefer to
# forward events to a centralized logging system that handles redaction.
# Redact sensitive patterns before logging.
# Adjust these patterns to match your organization's needs.
$redactedToolArgs = $toolArgsRaw `
-replace 'ghp_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'gho_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'ghu_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'ghs_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'Bearer [A-Za-z0-9_\-\.]+', 'Bearer [REDACTED]' `
-replace '--password[= ][^ ]+', '--password=[REDACTED]' `
-replace '--token[= ][^ ]+', '--token=[REDACTED]'
# Log attempted tool use with redacted toolArgs.
(@{
event = "preToolUse"
toolName = $toolName
toolArgs = $redactedToolArgs
} | ConvertTo-Json -Compress) | Add-Content -Path "$logDir/audit.jsonl"
if ($toolName -ne "bash") { exit 0 }
# Parse toolArgs JSON string.
$toolArgs = $null
try { $toolArgs = $toolArgsRaw | ConvertFrom-Json } catch { exit 0 }
$command = $toolArgs.command
# ---------------------------------------------------------------------------
# Demo-only deny rule for safe testing.
# This blocks a harmless test command so you can validate the deny flow.
# Remove this rule after confirming your hooks work as expected.
# ---------------------------------------------------------------------------
if ($command -match 'COPILOT_HOOKS_DENY_DEMO') {
Deny "Blocked demo command (test rule). Remove this rule after validating hooks."
}
function Deny([string]$reason) {
# Redact sensitive patterns from command before logging.
$redactedCommand = $command `
-replace 'ghp_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'gho_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'ghu_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'ghs_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
-replace 'Bearer [A-Za-z0-9_\-\.]+', 'Bearer [REDACTED]' `
-replace '--password[= ][^ ]+', '--password=[REDACTED]' `
-replace '--token[= ][^ ]+', '--token=[REDACTED]'
# Log the denial decision with redacted command.
(@{
event = "policyDeny"
toolName = "bash"
command = $redactedCommand
reason = $reason
} | ConvertTo-Json -Compress) | Add-Content -Path "$logDir/audit.jsonl"
(@{
permissionDecision = "deny"
permissionDecisionReason = $reason
} | ConvertTo-Json -Compress)
exit 0
}
if ($command -match '\b(sudo|su|runas)\b') { Deny "Privilege escalation requires manual approval." }
if ($command -match 'rm\s+-rf\s*/(\s|$)|rm\s+.*-rf\s*/(\s|$)') { Deny "Destructive operations targeting the filesystem root require manual approval." }
if ($command -match '\b(mkfs|dd|format)\b') { Deny "System-level destructive operations are not allowed via automated execution." }
if ($command -match 'curl.*\|\s*(bash|sh)|wget.*\|\s*(bash|sh)') { Deny "Download-and-execute patterns require manual approval." }
exit 0
测试策略脚本
你可以通过管道传入示例 preToolUse 输入来测试脚本。
允许示例:
echo '{"toolName":"bash","toolArgs":"{\"command\":\"git status\"}"}' \
| .github/hooks/scripts/pre-tool-policy.sh
# or, for PowerShell
echo '{"toolName":"bash","toolArgs":"{\"command\":\"git status\"}"}' |
.github/hooks/scripts/pre-tool-policy.ps1
拒绝示例:
echo '{"toolName":"bash","toolArgs":"{\"command\":\"sudo rm -rf /\"}"}' \
| .github/hooks/scripts/pre-tool-policy.sh
# or, for PowerShell
echo '{"toolName":"bash","toolArgs":"{\"command\":\"sudo rm -rf /\"}"}' |
.github/hooks/scripts/pre-tool-policy.ps1
运行拒绝示例后,检查.github/hooks/logs/audit.jsonl是否有新的拒绝日志条目。
{"permissionDecision":"deny","permissionDecisionReason":"Privilege escalation requires manual approval."}
此时,会阻止高风险 bash 命令在此存储库中自动执行。
6. 在存储库中端到端测试
在创建配置文件和脚本后,验证在该仓库中使用 Copilot 命令行界面(CLI) 时 hooks 是否按预期运行。
验证挂钩配置文件
检查挂钩配置文件是否为有效的 JSON:
jq '.' < .github/hooks/copilot-cli-policy.json
jq '.' < .github/hooks/copilot-cli-policy.json
验证脚本权限(基于 Unix 的系统)
在 macOS 和 Linux 上,确认 Bash 脚本是可执行的:
chmod +x .github/hooks/scripts/*.sh
chmod +x .github/hooks/scripts/*.sh
运行基本会话
在仓库中启动一个新的 Copilot 命令行界面(CLI) 会话:
copilot -p "Show me the status of this repository"
copilot -p "Show me the status of this repository"
预期结果:
- 你将看到策略横幅(来自
sessionStart)。 - 将新条目添加到
.github/hooks/logs/audit.jsonl(从userPromptSubmitted)。
触发工具的使用与日志记录验证
运行提示以使 数据变量.copilot.copilot_cli_short %} 使用工具(例如 bash):
copilot -p "Show me the last 5 git commits"
copilot -p "Show me the last 5 git commits"
预期结果:
-
`preToolUse` 条目将添加到 `.github/hooks/logs/audit.jsonl`。 - 如果允许工具调用,则执行将正常进行。
测试被拒绝的命令
示例策略脚本包括一个临时演示规则,用于阻止包含字符串 COPILOT_HOOKS_DENY_DEMO的命令。 这样就可以安全地验证拒绝流,而无需运行破坏性命令。
运行一个会导致命令被拒绝的提示:
copilot -p "Run a test command: echo COPILOT_HOOKS_DENY_DEMO"
copilot -p "Run a test command: echo COPILOT_HOOKS_DENY_DEMO"
预期结果:
- Copilot 命令行界面(CLI) 不会执行该命令。
- 钩子函数返回一个拒绝响应,并附有明确的原因。
-
`policyDeny` 条目写入 `.github/hooks/logs/audit.jsonl`。
确认拒绝流正常工作后,请从脚本中删除演示规则,并将其替换为反映组织策略的拒绝模式。
检查审核日志
查看最近的条目:
tail -n 50 .github/hooks/logs/audit.jsonl
tail -n 50 .github/hooks/logs/audit.jsonl
仅筛选被拒绝的决定:
jq 'select(.event=="policyDeny")' .github/hooks/logs/audit.jsonl
jq 'select(.event=="policyDeny")' .github/hooks/logs/audit.jsonl
7. 跨团队安全部署
在单个存储库中验证钩子后,逐步推出以避免中断开发工作流。
选择推出策略
常见的推出方法包括:
-
**日志记录优先推出(建议)**:首先记录提示和工具使用情况,而无需拒绝执行。 查看一段时间的日志,然后在了解常见使用模式后引入拒绝规则。 -
**团队部署**:一次将挂钩部署到一个团队或存储库,收集反馈,然后扩展到其他团队。 -
**基于风险的推出**:从处理敏感系统或生产基础结构的存储库开始,然后扩展到风险较低的存储库。
传达期望
在强制实施拒绝规则之前,请确保开发人员了解:
- 这表明 hooks 已在仓库中处于活动状态
- 可以阻止哪些类型的命令
- 如果执行命令被拒绝,应如何继续操作
清晰的沟通从而减少混淆和支持请求。
使策略保持可维护
随着使用情况的发展:
- 将挂钩配置和脚本存储在版本控制中。
- 定期查看审核日志,以检测新的风险模式。
- 请逐步更新拒绝规则,而不要一次性添加范围过大的匹配规则。
- 记录每个拒绝规则存在的原因,尤其是对于高影响限制。
小心处理异常
某些团队(例如基础结构或平台团队)可能需要更广泛的权限。 若要安全地处理此问题,
- 维护不同存储库的单独挂钩配置。
- 确保例外范围较窄且记录完善。
- 避免临时性的本地绕过,这会削弱审计透明度。
延伸阅读
有关 hooks 的故障排查,请参阅 将钩子与 GitHub Copilot 代理配合使用。