DEV Community

Daniel Jonathan
Daniel Jonathan

Posted on

Teaching Coding Agent to Write XSLT — The Hook Chain

Part 1 covered the XSLT skill — domain knowledge that prevents mistakes upfront. This part covers the runtime side: two PostToolUse hooks that automate compile-and-run so Claude sees the result of its own edits immediately.

Claude writes a file → hooks fire → output (or error) appears in context → Claude fixes and repeats. When the debugger is running and a matching launch config exists, no manual runs are needed. Otherwise the hook reports what's missing and Claude can guide you.


PostToolUse hooks

Claude Code hooks are shell commands that fire after tool events. PostToolUse fires after every Write or Edit — meaning iterative fixes get immediate feedback on every change.

Configure in .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..."
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Both hooks filter on file extension internally — the first acts only on .lml, the second only on .xslt.


Hook 1: LML compile

When Claude edits a .lml file, the designer isn't involved — the compiled .xslt would be stale. This hook compiles it and runs the transform in one step.

The compiler: lml-compile

The Logic Apps testing SDK includes DataMapTestExecutor with a GenerateXslt() method. I wrapped it as a .NET global tool:

dotnet tool install -g lml-compile
Enter fullscreen mode Exit fullscreen mode

No running Logic Apps host. No Azurite. Just:

lml-compile input.lml output.xslt
Enter fullscreen mode Exit fullscreen mode

The hook

#!/usr/bin/env bash
set -euo pipefail

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

# Find workspace and run transform via debugger HTTP API
# (walks up from file to find .vscode/launch.json)
# ... matches launch config, calls POST /run-transform ...

jq -n --arg ctx "$OUTPUT" '{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": ("LML compiled + transform result:\n" + $ctx)
  }
}'
Enter fullscreen mode Exit fullscreen mode

If compile fails, Claude sees the error. If it succeeds and the debugger is running with a matching launch config, Claude sees the transform output in the same turn. If the debugger isn't running or no config matches, the hook reports the compile success and what's missing.


Hook 2: Transform runner

Fires when Claude writes or edits a .xslt file directly (Generate mode).

The key challenge: workspace derivation. In hook context, $(pwd) doesn't point to the project — it can resolve to /private/tmp. The fix: walk up from the edited file's path to find .vscode/launch.json:

DIR="$(cd "$(dirname "$EDITED_FILE")" && pwd)"
WORKSPACE=""
while [[ "$DIR" != "/" ]]; do
  if [[ -f "$DIR/.vscode/launch.json" ]]; then
    WORKSPACE="$DIR"; break
  fi
  DIR="$(dirname "$DIR")"
done
Enter fullscreen mode Exit fullscreen mode

The hook then reads the matching launch config (resolving ${workspaceFolder}), extracts the input XML and engine, and calls the XSLT Debugger's 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\"}")
Enter fullscreen mode Exit fullscreen mode

The result is returned via hookSpecificOutput so Claude sees the transform output in context.


How the hooks interact

Claude writes .xslt (Generate mode):

Claude → Write .xslt → Hook 1 ignores (not .lml) → Hook 2 runs transform → result to Claude
Enter fullscreen mode Exit fullscreen mode

Claude writes .lml (LML mode):

Claude → Write .lml → Hook 1 compiles + runs transform → result to Claude → Hook 2 ignores (not .xslt)
Enter fullscreen mode Exit fullscreen mode

The .xslt file written by lml-compile does not trigger Hook 2 — only files Claude writes directly via Write or Edit trigger hooks.


The feedback loop

Claude writes XSLT → hook runs transform → output in context → error? → Claude edits → hook runs again → correct output
Enter fullscreen mode Exit fullscreen mode

No human in the loop for the fix cycle. Claude writes, sees the result, fixes if needed, sees the result again.


Shell bugs worth knowing

set -e with || true: Using OUTPUT=$(cmd) || true resets $? to 0 — the failure branch is unreachable. Fix: set +e, capture $?, then set -e.

grep -v returns exit 1 when no lines match: Under set -euo pipefail, this kills the hook. Append || true to the grep specifically.

$(pwd) in hook context: Doesn't inherit the project directory. Always derive workspace from the edited file's path.


The pattern generalises

The pattern is: skill (domain rules) + hook (verify after every write) = self-correcting AI. Nothing here is XSLT-specific except the domain knowledge and the tools the hooks call.

The same approach works for Terraform (terraform validate + terraform plan after .tf edits), SQL migrations, OpenAPI specs — anything where a write can be verified automatically.


The XSLT Debugger extension is on the VS Code Marketplace for macOS and Windows.

Top comments (0)