Claude Code에게 XSLT 작성법을 가르치기 — The Hook Chain
Source: Dev.to
개요
Part 1에서는 XSLT 기술을 소개했습니다—실수를 사전에 방지하는 도메인 지식입니다.
Part 2는 런타임 측면에 초점을 맞춥니다: 두 개의 PostToolUse 훅이 XSLT를 자동으로 컴파일하고 실행하여 Claude가 자신의 편집 결과를 즉시 확인할 수 있게 합니다.
워크플로우
- Claude가 파일을 작성합니다.
- 훅이 실행됩니다.
- 출력(또는 오류)이 Claude의 컨텍스트에 표시됩니다.
- Claude가 파일을 수정하고 다시 실행합니다.
디버거가 실행 중이고 일치하는 런치 구성이 존재하면 수동 실행이 필요하지 않습니다. 그렇지 않으면 훅이 누락된 부분을 보고하고 Claude가 안내할 수 있습니다.
Claude Code 훅
Claude Code 훅은 도구 이벤트 이후에 실행되는 셸 명령입니다. PostToolUse는 모든 Write 또는 Edit 후에 실행되어 각 변경에 대한 즉각적인 피드백을 제공합니다.
훅 설정 (.claude/settings.json)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/generate-xslt-from-lml.sh",
"timeout": 45,
"statusMessage": "Compiling LML + running transform..."
},
{
"type": "command",
"command": "bash .claude/hooks/run-xslt-after-edit.sh",
"timeout": 30,
"statusMessage": "Running XSLT transform..."
}
]
}
]
}
}
두 훅은 내부적으로 파일 확장자를 기준으로 필터링합니다:
- generate‑xslt‑from‑lml.sh –
.lml파일에만 작동합니다. - run‑xslt‑after‑edit.sh –
.xslt파일에만 작동합니다.
Claude가 .lml 파일을 편집하면, 컴파일된 .xslt 파일이 오래될 수 있습니다. 첫 번째 훅은 이를 컴파일하고 변환을 한 번에 실행합니다.
LML Compile Tool
Logic Apps 테스트 SDK에는 DataMapTestExecutor와 GenerateXslt() 메서드가 포함되어 있습니다. 이는 .NET 전역 도구로 래핑되어 있습니다:
dotnet tool install -g lml-compile
Logic Apps 호스트나 Azurite가 필요하지 않습니다. 다음과 같이 사용하세요:
lml-compile input.lml output.xslt
Source:
Hook 1 – Compile LML and Run Transform (generate-xslt-from-lml.sh)
#!/usr/bin/env bash
set -euo pipefail
# Read the tool input JSON from stdin
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only act on .lml files
case "$FILE_PATH" in
*.lml) ;;
*) exit 0 ;;
esac
# Derive output path
BASENAME=$(basename "$FILE_PATH" .lml)
MAPS_DIR="$(cd "$(dirname "$FILE_PATH")/.." && pwd)/Maps"
OUT_XSLT="${MAPS_DIR}/${BASENAME}.xslt"
mkdir -p "$MAPS_DIR"
# Compile
set +e
COMPILE_OUTPUT=$(lml-compile "$FILE_PATH" "$OUT_XSLT" 2>&1)
COMPILE_EXIT=$?
set -e
if [ $COMPILE_EXIT -ne 0 ]; then
jq -n --arg err "$COMPILE_OUTPUT" '{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": ("LML compile failed: " + $err)
}
}'
exit 0
fi
# If the debugger is running, invoke its HTTP API to run the transform.
# (The script walks up from the file to locate .vscode/launch.json,
# matches a launch config, and POSTs to /run-transform.)
# Placeholder for actual API call – replace with real implementation.
OUTPUT="(transform output placeholder)"
jq -n --arg ctx "$OUTPUT" '{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": ("LML compiled + transform result:\n" + $ctx)
}
}'
컴파일에 실패하면 Claude가 오류를 받습니다.
컴파일에 성공하고 일치하는 디버거 런치 설정이 활성화되어 있으면 Claude가 같은 턴에 변환 결과를 확인합니다.
디버거가 실행 중이 아니거나 일치하는 설정이 없을 경우, 훅은 컴파일 성공과 누락된 항목을 보고합니다.
Hook 2 – 직접 편집 후 XSLT 실행 (run-xslt-after-edit.sh)
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only act on .xslt files
case "$FILE_PATH" in
*.xslt) ;;
*) exit 0 ;;
esac
# Derive workspace by walking up to find .vscode/launch.json
DIR="$(cd "$(dirname "$FILE_PATH")" && pwd)"
WORKSPACE=""
while [[ "$DIR" != "/" ]]; do
if [[ -f "$DIR/.vscode/launch.json" ]]; then
WORKSPACE="$DIR"
break
fi
DIR="$(dirname "$DIR")"
done
if [[ -z "$WORKSPACE" ]]; then
jq -n '{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "No launch configuration found."
}
}'
exit 0
fi
# Extract launch config values (placeholder – adapt to your config)
# Example assumes variables: ${workspaceFolder}, input XML, engine, port.
LAUNCH_CONFIG=$(jq -r '.configurations[] | select(.type=="xslt-debugger")' "$WORKSPACE/.vscode/launch.json")
PORT=$(echo "$LAUNCH_CONFIG" | jq -r '.port')
STYLESHEET="$FILE_PATH"
XML=$(echo "$LAUNCH_CONFIG" | jq -r '.xml')
ENGINE=$(echo "$LAUNCH_CONFIG" | jq -r '.engine')
# Call the XSLT Debugger HTTP API
RESULT=$(curl -s -X POST "http://127.0.0.1:$PORT/run-transform" \
-H "Content-Type: application/json" \
-d "{\"stylesheet\": \"$STYLESHEET\", \"xml\": \"$XML\", \"engine\": \"$ENGINE\"}")
jq -n --arg ctx "$RESULT" '{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": ("XSLT transform result:\n" + $ctx)
}
}'
핵심 과제는 워크스페이스 파생입니다—훅 내부의 $(pwd)가 임시 디렉터리(예: /private/tmp)를 가리킬 수 있습니다. 스크립트는 편집된 파일 경로에서 위로 올라가 프로젝트의 .vscode/launch.json을 찾습니다.
Interaction Diagrams
Generate Mode (Claude edits .xslt directly)
Claude → Write .xslt → Hook 1 ignores (not .lml) → Hook 2 runs transform → Result returned to Claude
LML Mode (Claude edits .lml)
Claude → Write .lml → Hook 1 compiles + runs transform → Result returned to Claude → Hook 2 ignores (not .xslt)
lml-compile에 의해 생성된 .xslt 파일은 Hook 2를 트리거하지 않으며, 이는 Claude가 Write 또는 Edit를 통해 작성한 파일만 훅이 작동하기 때문입니다.
일반적인 함정 및 해결책
| 문제 | 발생 원인 | 해결책 |
|---|---|---|
set -e와 `OUTPUT=$(cmd) | true`를 함께 사용하면 실패 분기가 도달할 수 없습니다 | |
grep -v가 일치하는 줄이 없을 때 종료 코드 1을 반환하여 set -euo pipefail 환경에서 후크가 중단됩니다 | 비‑0 종료가 실패로 간주됩니다 | 전체 파이프라인이 아니라 grep 명령에만 ` |
$(pwd)가 후크 컨텍스트에서 임시 디렉터리를 가리킵니다 | 후크는 프로젝트 작업 디렉터리가 아닌 격리된 프로세스에서 실행됩니다 | 편집된 파일 경로에서 작업 공간을 파생합니다 (Hook 2 참조). |
일반 패턴
skill (domain rules) + hook (verify after every write) = self‑correcting AI
이 접근 방식은 XSLT에만 국한된 것이 아니라, 쓰기 후 자동으로 검증할 수 있는 모든 아티팩트에 적용됩니다.
기타 예시
- Terraform –
.tf파일을 수정한 뒤terraform validate+terraform plan실행. - SQL 마이그레이션 – 테스트 데이터베이스에 마이그레이션을 적용해 실행.
- OpenAPI 스펙 – 린트(lint)하고 클라이언트 스텁을 생성.
XSLT 디버거 확장
XSLT 디버거 확장은 macOS 및 Windows용 VS Code Marketplace에서 제공됩니다. 이 확장은 Hook 2가 변환을 실행하고 결과를 반환하는 데 사용하는 HTTP API를 제공합니다.