diff --git a/.husky/.gitattributes b/.husky/.gitattributes
new file mode 100644
index 000000000..fcadb2cf9
--- /dev/null
+++ b/.husky/.gitattributes
@@ -0,0 +1 @@
+* text eol=lf
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100644
index 000000000..1a089f456
--- /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 e992b0c44..f07319409 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,6 +44,7 @@
         "file-loader": "2.0.0",
         "format-message-cli": "6.2.0",
         "gh-pages": "1.2.0",
+        "husky": "8.0.1",
         "in-publish": "2.0.1",
         "js-md5": "0.7.3",
         "jsdoc": "3.6.6",
@@ -13957,6 +13958,21 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/husky": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz",
+      "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==",
+      "dev": true,
+      "bin": {
+        "husky": "lib/bin.js"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/typicode"
+      }
+    },
     "node_modules/iconv-lite": {
       "version": "0.4.19",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
@@ -37522,6 +37538,12 @@
         "strip-url-auth": "^1.0.0"
       }
     },
+    "husky": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz",
+      "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==",
+      "dev": true
+    },
     "iconv-lite": {
       "version": "0.4.19",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
diff --git a/package.json b/package.json
index 29c28fd69..955edc463 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
     "i18n:src": "mkdirp translations/core && format-message extract --out-file translations/core/en.json src/extensions/**/index.js",
     "i18n:push": "tx-push-src scratch-editor extensions translations/core/en.json",
     "lint": "eslint . && format-message lint src/**/*.js",
+    "prepare": "husky install",
     "prepublish": "in-publish && npm run build || not-in-publish",
     "start": "webpack-dev-server",
     "tap": "tap ./test/{unit,integration}/*.js",
@@ -67,6 +68,7 @@
     "file-loader": "2.0.0",
     "format-message-cli": "6.2.0",
     "gh-pages": "1.2.0",
+    "husky": "8.0.1",
     "in-publish": "2.0.1",
     "js-md5": "0.7.3",
     "jsdoc": "3.6.6",