regex hook
The regex hook is the deterministic regex-safety engine the regex sub-skill runs, and you can run it yourself. It works in two modes from one command: a scan that sweeps a codebase for ReDoS-prone and runtime-built patterns, and a check that scores a single pattern you hand it.
Scan a codebase
With no flag it scans the whole project. -d scopes the scan to a directory, -f to a single file. Both repeat and combine into one report, each file listed once even when more than one target covers it.
- Whole project
- A directory
- A single file
- Several targets
- With a limit
node ./.lagune/hooks/regex.mjs
node ./.lagune/hooks/regex.mjs -d src/services
node ./.lagune/hooks/regex.mjs -f bin/import.pl
node ./.lagune/hooks/regex.mjs -d core -d workers -f Sources/App/Router.swift
-l sets the ReDoS threshold for the sweep (default 25), the same limit the check mode takes. A lower limit is stricter.
node ./.lagune/hooks/regex.mjs -d src/services -l 15
The output is raw text whose headers say how to read each section. A section is shown only when it has findings.
Vulnerable regular expressions found:
cmd/server/main.go
([a-z]+)+$
Dynamically built regular expressions (review manually):
app/payments.php
crates/parser/lexer.rs
lib/validate.py
Static regex wrapped in a constructor (use a literal instead):
config/token.rb
When nothing is found, it prints a single line.
node ./.lagune/hooks/regex.mjs -d src/clean
# => no unsafe patterns found
How to read the sections
| Section | Meaning |
|---|---|
Vulnerable regular expressions found | ReDoS-prone patterns, grouped by file with each pattern indented beneath. Every line is unsafe, so the verdict is not repeated. |
Dynamically built regular expressions (review manually) | Files that build a regex from a variable, a concatenation, or an interpolation. Not a finding on its own: read each by hand. |
Static regex wrapped in a constructor (use a literal instead) | Files that pass a plain string literal to a regex constructor where a regex literal is simpler and skips a runtime compile. |
The first section is a security finding. The second is a pointer to code a static check cannot judge, so it asks for a human read. The third is a clarity hint, not a security finding.
CLI options
| Option | Alias | Value | Description |
|---|---|---|---|
--pattern | -p | a regex source | Check one pattern. Repeat to check several, one verdict per pattern, in order. |
--dir | -d | a directory | Scope a scan to a directory. Repeats and combines with -f. |
--file | -f | a file | Scope a scan to a single file. Repeats and combines with -d. |
--limit | -l | a non-negative integer | Set the ReDoS repetition limit (default 25). Applies to both modes. A lower limit is stricter. |
With no option it scans the whole project. -p is the check mode and cannot be combined with -d or -f.
Supported languages
The scan reads these languages, keyed by file extension, each by its own rules so a pattern from one is never tested against another. Everything else is skipped rather than scanned with the wrong rules.
| # | Language | Extensions |
|---|---|---|
| 1 | JavaScript / TypeScript | .js, .jsx, .mjs, .cjs, .ts, .tsx, .mts, .cts, .astro, .vue, .svelte, .marko, .riot |
| 2 | Python | .py, .pyi |
| 3 | Ruby | .rb |
| 4 | Go | .go |
| 5 | PHP | .php |
| 6 | Rust | .rs |
| 7 | Java | .java |
| 8 | C# | .cs |
| 9 | C | .c, .h |
| 10 | C++ | .cc, .cpp, .cxx, .hpp, .hh, .hxx |
| 11 | Kotlin | .kt, .kts |
| 12 | Swift | .swift |
| 13 | Scala | .scala, .sc |
| 14 | Dart | .dart |
| 15 | PowerShell | .ps1, .psm1, .psd1 |
| 16 | Elixir | .ex, .exs |
| 17 | Objective-C | .m, .mm |
| 18 | R | .r |
| 19 | Julia | .jl |
| 20 | Clojure | .clj, .cljs, .cljc, .edn |
| 21 | Crystal | .cr |
| 22 | Nim | .nim, .nims |
| 23 | V | .v |
| 24 | D | .d |
| 25 | Perl | .pl, .pm |
It reads source as text, so it cannot judge a pattern assembled at runtime, and a pattern spread across lines, hidden behind an alias, or written in a form it does not recognize can slip past. Treat the three sections as a strong starting point, not a complete inventory, and keep reading the code for what they cannot reach.
Check a single pattern
Pass -p with the pattern's source to score it. The hook prints one word: safe, unsafe, or invalid regex. Repeat -p to score several at once, one verdict per pattern, in order.
- One pattern
- Several patterns
- With a limit
node ./.lagune/hooks/regex.mjs -p '(a+)+$'
# => unsafe
node ./.lagune/hooks/regex.mjs -p '^[a-z0-9_]{3,20}$'
# => safe
node ./.lagune/hooks/regex.mjs -p '(a+)+$' -p '^[a-z]+$'
# => unsafe
# => safe
-l sets the repetition limit (default 25) and applies to every -p in the call. A lower limit is stricter.
node ./.lagune/hooks/regex.mjs -p 'a?a?a?' -l 2
# => unsafe
Each pattern is passed as a flag value, never interpolated into the command. A value with quotes or backticks stays inert and cannot inject into the shell. Always wrap the pattern in single quotes so your shell does not expand it first.
This is the same engine the regex sub-skill runs: the scan opens a ReDoS pass, the check confirms a pattern it surfaces.
