diff --git a/.husky/.gitattributes b/.husky/.gitattributes
new file mode 100644
index 00000000..fcadb2cf
--- /dev/null
+++ b/.husky/.gitattributes
@@ -0,0 +1 @@
+* text eol=lf
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100755
index 00000000..80416c7b
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npx --no-install commitlint --edit "$1"
diff --git a/package-lock.json b/package-lock.json
index 918ccc26..0210244c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
         "glob": "7.2.3",
         "google-closure-compiler": "20180402.0.0",
         "graceful-fs": "4.2.11",
+        "husky": "8.0.3",
         "json": "9.0.6",
         "rimraf": "2.7.1",
         "selenium-webdriver": "4.14.0",
@@ -5535,6 +5536,21 @@
         "node": ">=10.17.0"
       }
     },
+    "node_modules/husky": {
+      "version": "8.0.3",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz",
+      "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
+      "dev": true,
+      "bin": {
+        "husky": "lib/bin.js"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/typicode"
+      }
+    },
     "node_modules/iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
diff --git a/package.json b/package.json
index 15e37b98..e838e081 100644
--- a/package.json
+++ b/package.json
@@ -13,16 +13,17 @@
   "browser": "./shim/vertical.js",
   "scripts": {
     "deploy": "rimraf gh-pages/closure-library/scripts/ci/CloseAdobeDialog.exe && gh-pages -t -d gh-pages -m \"Build for $(git log --pretty=format:%H -n1) [skip ci]\"",
+    "prepare": "husky install",
     "prepublish": "python build.py && webpack",
-    "test:unit": "node tests/jsunit/test_runner.js",
+    "test": "npm run test:messages && npm run test:unit",
     "test:lint": "eslint .",
     "test:messages": "npm run translate && node i18n/test_scratch_msgs.js",
-    "test": "npm run test:messages && npm run test:unit",
-    "version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"",
+    "test:unit": "node tests/jsunit/test_runner.js",
     "translate": "node i18n/js_to_json.js && node i18n/json_to_js.js",
     "translate:sync:src": "tx-push-src scratch-editor blocks msg/json/en.json",
     "translate:sync:translations": "node i18n/sync_tx_translations.js",
-    "translate:update": "npm run translate:sync:src && npm run translate:sync:translations"
+    "translate:update": "npm run translate:sync:src && npm run translate:sync:translations",
+    "version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
   },
   "dependencies": {
     "chromedriver": "^118.0.1",
@@ -42,6 +43,7 @@
     "glob": "7.2.3",
     "google-closure-compiler": "20180402.0.0",
     "graceful-fs": "4.2.11",
+    "husky": "8.0.3",
     "json": "9.0.6",
     "rimraf": "2.7.1",
     "selenium-webdriver": "4.14.0",