diff --git a/.gitattributes b/.gitattributes
index c5b9ea10e13490a9ba4f35557d3beb84e6f52212..1bea4dc8f2b0708c2fa0d8c3f277c8a61f779419 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1 @@
-/build/webpack.prod.conf.js export-subst
+/build/commit_hash.js export-subst
diff --git a/build/commit_hash.js b/build/commit_hash.js
new file mode 100644
index 0000000000000000000000000000000000000000..c104af5d9a45618d10c5f54e74a841d173080694
--- /dev/null
+++ b/build/commit_hash.js
@@ -0,0 +1,18 @@
+import childProcess from 'child_process'
+
+export const getCommitHash = (() => {
+  const subst = "$Format:%h$"
+  if(!subst.match(/Format:/)) {
+    return subst
+  } else {
+    try {
+      return childProcess
+        .execSync('git rev-parse --short HEAD')
+        .toString()
+        .trim()
+    } catch (e) {
+      console.error('Failed run git:', e)
+      return 'UNKNOWN'
+    }
+  }
+})
diff --git a/vite.config.js b/vite.config.js
index b308c98c95002578f43cfd7fd2ed4eca7c55215b..82e822581e0fe892d0c1648c463a5a50b5db0d62 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -7,6 +7,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 import { VitePWA } from 'vite-plugin-pwa'
 import { devSwPlugin, buildSwPlugin, swMessagesPlugin } from './build/sw_plugin.js'
 import copyPlugin from './build/copy_plugin.js'
+import { getCommitHash } from './build/commit_hash.js'
 
 const localConfigPath = '<projectRoot>/config/local.json'
 const getLocalDevSettings = async () => {
@@ -116,7 +117,7 @@ export default defineConfig(async ({ mode, command }) => {
         NODE_ENV: command === 'serve' ? 'development' : 'production',
         HAS_MODULE_SERVICE_WORKER: command === 'serve' && !transformSW
       }),
-      'COMMIT_HASH': JSON.stringify('DEV'),
+      'COMMIT_HASH': JSON.stringify(command === 'serve' ? 'DEV' : getCommitHash()),
       'DEV_OVERRIDES': JSON.stringify({})
     },
     build: {