{
"event": "PreToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": "\/var\/www\/dev.campus.systemische-tools.de\/crawler\/scripts\/htmx_lint.py",
"old_string": "\"\"\"HTMX-compliance linter — enforces rules HTMX-C1..C5 from CLAUDE.md.",
"new_string": "#!\/usr\/bin\/env python3\n\"\"\"HTMX-compliance linter — enforces rules HTMX-C1..C5 from CLAUDE.md.",
"replace_all": false
}
}
{
"tool_response": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/crawler\/scripts\/htmx_lint.py",
"oldString": "\"\"\"HTMX-compliance linter — enforces rules HTMX-C1..C5 from CLAUDE.md.",
"newString": "#!\/usr\/bin\/env python3\n\"\"\"HTMX-compliance linter — enforces rules HTMX-C1..C5 from CLAUDE.md.",
"originalFile": "\"\"\"HTMX-compliance linter — enforces rules HTMX-C1..C5 from CLAUDE.md.\n\nRules:\n- HTMX-C1: hx-post MUST have hx-headers containing X-CSRF-TOKEN\n- HTMX-C2: hx-delete MUST have hx-headers containing X-CSRF-TOKEN\n- HTMX-C3: hx-patch MUST have hx-headers containing X-CSRF-TOKEN\n- HTMX-C4: hx-delete MUST have hx-confirm\n- HTMX-C5: hx-put MUST have hx-headers containing X-CSRF-TOKEN\n\nUsage:\n python scripts\/htmx_lint.py <path> [<path> ...]\n\nExit codes:\n 0 OK\n 1 violation(s)\n\"\"\"\nfrom __future__ import annotations\n\nimport pathlib\nimport re\nimport sys\nfrom collections.abc import Iterable\nfrom dataclasses import dataclass\n\nMUTATING_VERBS = (\"post\", \"put\", \"patch\", \"delete\")\nTEMPLATE_GLOBS = (\"*.php\", \"*.phtml\", \"*.html\", \"*.tpl\")\n\n\n@dataclass(frozen=True)\nclass Finding:\n file: pathlib.Path\n lineno: int\n rule: str\n message: str\n\n\n_TAG_RE = re.compile(r\"<[^>]*\\bhx-(post|put|patch|delete)\\b[^>]*>\", re.IGNORECASE | re.DOTALL)\n\n\ndef _iter_template_files(paths: Iterable[str]) -> Iterable[pathlib.Path]:\n for raw in paths:\n base = pathlib.Path(raw)\n if base.is_file():\n yield base\n elif base.is_dir():\n for pattern in TEMPLATE_GLOBS:\n yield from base.rglob(pattern)\n\n\ndef _check_tag(tag: str) -> list[str]:\n lowered = tag.lower()\n verb_match = re.search(r\"\\bhx-(post|put|patch|delete)\\b\", lowered)\n if not verb_match:\n return []\n verb = verb_match.group(1)\n failures: list[str] = []\n has_csrf = \"hx-headers\" in lowered and \"x-csrf-token\" in lowered\n if verb == \"post\" and not has_csrf:\n failures.append(\"HTMX-C1: hx-post without X-CSRF-TOKEN\")\n if verb == \"patch\" and not has_csrf:\n failures.append(\"HTMX-C3: hx-patch without X-CSRF-TOKEN\")\n if verb == \"put\" and not has_csrf:\n failures.append(\"HTMX-C5: hx-put without X-CSRF-TOKEN\")\n if verb == \"delete\":\n if not has_csrf:\n failures.append(\"HTMX-C2: hx-delete without X-CSRF-TOKEN\")\n if \"hx-confirm\" not in lowered:\n failures.append(\"HTMX-C4: hx-delete without hx-confirm\")\n return failures\n\n\ndef _lineno_of(text: str, offset: int) -> int:\n return text.count(\"\\n\", 0, offset) + 1\n\n\ndef check_file(path: pathlib.Path) -> list[Finding]:\n findings: list[Finding] = []\n try:\n text = path.read_text(encoding=\"utf-8\")\n except UnicodeDecodeError:\n return findings\n for match in _TAG_RE.finditer(text):\n tag = match.group(0)\n for fail in _check_tag(tag):\n rule_id, _, message = fail.partition(\":\")\n findings.append(Finding(path, _lineno_of(text, match.start()), rule_id, message.strip()))\n return findings\n\n\ndef main(argv: list[str]) -> int:\n if not argv:\n print(\"usage: htmx_lint.py <path> [...]\", file=sys.stderr)\n return 1\n findings: list[Finding] = []\n for path in _iter_template_files(argv):\n findings.extend(check_file(path))\n for f in findings:\n print(f\"{f.file}:{f.lineno}: {f.rule}: {f.message}\", file=sys.stderr)\n return 1 if findings else 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main(sys.argv[1:]))\n",
"structuredPatch": [
{
"oldStart": 1,
"oldLines": 3,
"newStart": 1,
"newLines": 4,
"lines": [
"+#!\/usr\/bin\/env python3",
" \"\"\"HTMX-compliance linter — enforces rules HTMX-C1..C5 from CLAUDE.md.",
" ",
" Rules:"
]
}
],
"userModified": false,
"replaceAll": false
}
}