diff --git a/devTools/macro_check.py b/devTools/macro_check.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf9a03cb7b263c90ab4225d17f23ede18f0f0edf
--- /dev/null
+++ b/devTools/macro_check.py
@@ -0,0 +1,157 @@
+import argparse
+import pathlib
+import re
+
+
+widget_regex = re.compile(r"""<<widget '?"?([\w\-_]+)"?'?""")
+macro_regex = re.compile(r"""Macro\.add\(["']([\w\-_]+)["']""")
+define_macro_regex = re.compile(r'DefineMacroS?\("([\w\-_]+)"')
+javascript_regex = re.compile(r"function ([\w\-_]+)\(")
+used_regex = re.compile(r"<</?([\w\-_]+).*?>>")
+comment_regex = re.compile(r"<!--[\s\S]*?-->")
+javascript_comment1_regex = re.compile(r"\s*//.*")
+javascript_comment2_regex = re.compile(r"\s*/\*[\s\S]*?\*/")
+
+default = {
+    "if",
+    "elseif",
+    "else",
+    "set",
+    "link",
+    "linkappend",
+    "linkprepend",
+    "linkreplace",
+    "case",
+    "widget",
+    "break",
+    "include",
+    "compute",
+    "nobr",
+    "return",
+    "for",
+    "silently",
+    "audio",
+    "cacheaudio",
+    "createaudiogroup",
+    "createplaylist",
+    "masteraudio",
+    "playlist",
+    "done",
+    "print",
+    "append",
+    "unset",
+    "switch",
+    "prepend",
+    "radiobutton",
+    "repeat",
+    "default",
+    "stop",
+    "run",
+    "replace",
+    "numberbox",
+    "cycle",
+    "copy",
+    "timed",
+    "checkbox",
+    "addclass",
+    "removeclass",
+    "continue",
+    "listbox",
+    "optionsfrom",
+    "option",
+    "capture",
+    "button",
+    "choice",
+    "remove",
+    "onchange",
+    "condition",
+    "dynamicblock",
+    "textbox",
+    "actions",
+    "back",
+    "safereplace",
+    "script",
+    "toggleclass",
+    "twinescript",
+    "-",
+    "goto",
+    "type",
+    "textarea",
+}
+
+
+def get_js_macros(path: pathlib.Path):
+    content = path.read_text(encoding="utf-8")
+    content = re.sub(javascript_comment1_regex, "", content)
+    content = re.sub(javascript_comment2_regex, "", content)
+    macros = set()
+    for i in re.findall(macro_regex, content):
+        macros.add(i)
+    for i in re.findall(define_macro_regex, content):
+        macros.add(i)
+    for i in re.findall(javascript_regex, content):
+        macros.add(i)
+    return macros
+
+
+def get_twee_macros(path: pathlib.Path):
+    content = path.read_text(encoding="utf-8")
+    content = re.sub(comment_regex, "", content)
+    content = re.sub(javascript_comment2_regex, "", content)
+    macros = set()
+    for i in re.findall(widget_regex, content):
+        macros.add(i)
+    return macros
+
+
+def check(path, macros):
+    content = path.read_text(encoding="utf-8")
+    if path.suffix == ".js":
+        content = re.sub(javascript_comment1_regex, "", content)
+        m = re.search(javascript_comment2_regex, content)
+        while m:
+            t = content[m.start() : m.end()]
+            content = content.replace(t, "\n" * t.count("\n"))
+            m = re.search(javascript_comment2_regex, content)
+    elif path.suffix == ".twee":
+        m = re.search(comment_regex, content)
+        while m:
+            t = content[m.start() : m.end()]
+            content = content.replace(t, "\n" * t.count("\n"))
+            m = re.search(comment_regex, content)
+        m = re.search(javascript_comment2_regex, content)
+        while m:
+            t = content[m.start() : m.end()]
+            content = content.replace(t, "\n" * t.count("\n"))
+            m = re.search(javascript_comment2_regex, content)
+    for i in re.finditer(used_regex, content):
+        if i.group(1).startswith("-"):
+            continue
+        if i.group(1).lower() not in macros:
+            n = content[: i.start()].count("\n") + 1
+            print(f"{str(path)}:{n}: {i.group(1)}")
+
+
+parser = argparse.ArgumentParser(description="Checks macros.")
+parser.add_argument("paths", nargs="*", type=pathlib.Path)
+
+
+args = parser.parse_args()
+if not args.paths:
+    print("No paths")
+    exit(1)
+paths: list[pathlib.Path] = args.paths
+
+targets = []
+macros = default
+for i in paths:
+    if i.suffix == ".js":
+        macros |= get_js_macros(i)
+        targets.append(i)
+    elif i.suffix == ".twee":
+        macros |= get_twee_macros(i)
+        targets.append(i)
+macros = set(map(str.lower, macros))
+
+for i in targets:
+    check(i, macros)