Repository: openai/codex
Pull request #384: fix: configure husky and lint-staged for pnpm monorepo
Author: login: alphajoop
Created: 2025-04-19T01:12:05Z | Merged: 2025-04-19T14:18:37Z

# Improve Developer Experience with Husky and lint-staged for pnpm Monorepo

## Summary
This PR enhances the developer experience by configuring Husky and lint-staged to work properly with our pnpm monorepo structure. It centralizes Git hooks at the root level and ensures consistent code quality across the project.

## Changes
- Centralized Husky and lint-staged configuration at the monorepo root
- Added pre-commit hook that runs lint-staged to enforce code quality
- Configured lint-staged to:
  - Format JSON, MD, and YAML files with Prettier
  - Lint and typecheck TypeScript files before commits
- Fixed release script in codex-cli package.json (changed "pmpm" to "npm publish")
- Removed duplicate Husky and lint-staged configurations from codex-cli package.json

## Benefits
- **Consistent Code Quality**: Ensures all committed code meets project standards
- **Automated Formatting**: Automatically formats code during commits
- **Early Error Detection**: Catches type errors and lint issues before they're committed
- **Centralized Configuration**: Easier to maintain and update in one place
- **Improved Collaboration**: Ensures consistent code style across the team

## Future Improvements
We could further enhance this setup by
**Commit Message Validation**: Add commitlint to enforce conventional commit messages


Diff:
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 000000000000..e02c24e2b5c2
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+pnpm lint-staged
\ No newline at end of file
diff --git a/codex-cli/.husky/_/husky.sh b/codex-cli/.husky/_/husky.sh
deleted file mode 100644
index a09c6caf7eec..000000000000
--- a/codex-cli/.husky/_/husky.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env sh
-if [ -z "$husky_skip_init" ]; then
-  debug () {
-    if [ "$HUSKY_DEBUG" = "1" ]; then
-      echo "husky (debug) - $1"
-    fi
-  }
-
-  readonly hook_name="$(basename -- "$0")"
-  debug "starting $hook_name..."
-
-  if [ "$HUSKY" = "0" ]; then
-    debug "HUSKY env variable is set to 0, skipping hook"
-    exit 0
-  fi
-
-  if [ -f ~/.huskyrc ]; then
-    debug "sourcing ~/.huskyrc"
-    . ~/.huskyrc
-  fi
-
-  readonly husky_skip_init=1
-  export husky_skip_init
-  sh -e "$0" "$@"
-  exitCode="$?"
-
-  if [ $exitCode != 0 ]; then
-    echo "husky - $hook_name hook exited with code $exitCode (error)"
-  fi
-
-  exit $exitCode
-fi
diff --git a/codex-cli/.husky/pre-commit b/codex-cli/.husky/pre-commit
deleted file mode 100644
index f052379ff9d4..000000000000
--- a/codex-cli/.husky/pre-commit
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env sh
-. "$(dirname -- "$0")/_/husky.sh"
-
-# Run lint-staged to check files that are about to be committed
-npm run pre-commit
diff --git a/codex-cli/.husky/pre-push b/codex-cli/.husky/pre-push
deleted file mode 100644
index 339152541598..000000000000
--- a/codex-cli/.husky/pre-push
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env sh
-. "$(dirname -- "$0")/_/husky.sh"
-
-# Run tests and type checking before pushing
-npm test && npm run typecheck
diff --git a/codex-cli/.lintstagedrc.json b/codex-cli/.lintstagedrc.json
deleted file mode 100644
index 54b8b0629771..000000000000
--- a/codex-cli/.lintstagedrc.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "*.{ts,tsx}": [
-    "eslint --fix",
-    "prettier --write"
-  ],
-  "*.{json,md,yml}": [
-    "prettier --write"
-  ]
-}
diff --git a/codex-cli/package.json b/codex-cli/package.json
index d5c99c4d0987..b0a005d06169 100644
--- a/codex-cli/package.json
+++ b/codex-cli/package.json
@@ -22,10 +22,8 @@
     "build:dev": "NODE_ENV=development node build.mjs --dev && NODE_OPTIONS=--enable-source-maps node dist/cli-dev.js",
     "release:readme": "cp ../README.md ./README.md",
     "release:version": "TS=$(date +%y%m%d%H%M) && sed -E -i'' -e \"s/\\\"0\\.1\\.[0-9]{10}\\\"/\\\"0.1.${TS}\\\"/g\" package.json src/utils/session.ts",
-    "release:build-and-publish": "pmpm run build && pmpm publish",
-    "release": "pnpm run release:readme && pnpm run release:version && pnpm install && pnpm run release:build-and-publish",
-    "prepare": "husky",
-    "pre-commit": "lint-staged"
+    "release:build-and-publish": "pnpm run build && npm publish",
+    "release": "pnpm run release:readme && pnpm run release:version && pnpm install && pnpm run release:build-and-publish"
   },
   "files": [
     "README.md",
@@ -73,7 +71,6 @@
     "eslint-plugin-react-refresh": "^0.4.19",
     "husky": "^9.1.7",
     "ink-testing-library": "^3.0.0",
-    "lint-staged": "^15.5.1",
     "prettier": "^2.8.7",
     "punycode": "^2.3.1",
     "ts-node": "^10.9.1",
diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs
deleted file mode 100644
index 85af6291e6b1..000000000000
--- a/lint-staged.config.mjs
+++ /dev/null
@@ -1,4 +0,0 @@
-export default {
-  "*.{js,jsx,ts,tsx}": ["pnpm prettier --write", "pnpm eslint --fix"],
-  "*.{json,md,yml,yaml}": ["pnpm prettier --write"],
-};
diff --git a/package.json b/package.json
index b6ce8ee443d8..a54db4ada41f 100644
--- a/package.json
+++ b/package.json
@@ -10,12 +10,14 @@
     "test": "pnpm --filter @openai/codex run test",
     "lint": "pnpm --filter @openai/codex run lint",
     "typecheck": "pnpm --filter @openai/codex run typecheck",
+    "changelog": "git-cliff --config cliff.toml --output CHANGELOG.ignore.md $LAST_RELEASE_TAG..HEAD",
     "prepare": "husky",
-    "changelog": "git-cliff --config cliff.toml --output CHANGELOG.ignore.md $LAST_RELEASE_TAG..HEAD"
+    "husky:add": "husky add"
   },
   "devDependencies": {
     "git-cliff": "^2.8.0",
     "husky": "^9.1.7",
+    "lint-staged": "^15.5.1",
     "prettier": "^3.5.3"
   },
   "resolutions": {
@@ -30,5 +32,14 @@
     "node": ">=22",
     "pnpm": ">=9.0.0"
   },
+  "lint-staged": {
+    "*.json": "prettier --write",
+    "*.md": "prettier --write",
+    ".github/workflows/*.yml": "prettier --write",
+    "**/*.{js,ts,tsx}": [
+      "pnpm --filter @openai/codex run lint",
+      "pnpm --filter @openai/codex run typecheck"
+    ]
+  },
   "packageManager": "pnpm@10.8.1"
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d4bee57d575d..1539652c8f94 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,6 +19,9 @@ importers:
       husky:
         specifier: ^9.1.7
         version: 9.1.7
+      lint-staged:
+        specifier: ^15.5.1
+        version: 15.5.1
       prettier:
         specifier: ^3.5.3
         version: 3.5.3
@@ -88,7 +91,7 @@ importers:
     devDependencies:
       '@eslint/js':
         specifier: ^9.22.0
-        version: 9.25.0
+        version: 9.24.0
       '@types/diff':
         specifier: ^7.0.2
         version: 7.0.2
@@ -137,9 +140,6 @@ importers:
       ink-testing-library:
         specifier: ^3.0.0
         version: 3.0.0(@types/react@18.3.20)
-      lint-staged:
-        specifier: ^15.5.1
-        version: 15.5.1
       prettier:
         specifier: ^2.8.7
         version: 2.8.8
@@ -344,8 +344,8 @@ packages:
     resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
 
-  '@eslint/js@9.25.0':
-    resolution: {integrity: sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==}
+  '@eslint/js@9.24.0':
+    resolution: {integrity: sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@humanwhocodes/config-array@0.13.0':
@@ -2304,8 +2304,8 @@ packages:
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
     hasBin: true
 
-  vite@6.3.2:
-    resolution: {integrity: sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==}
+  vite@6.3.1:
+    resolution: {integrity: sha512-kkzzkqtMESYklo96HKKPE5KKLkC1amlsqt+RjFMlX2AvbRB/0wghap19NdBxxwGZ+h/C6DLCrcEphPIItlGrRQ==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
     hasBin: true
     peerDependencies:
@@ -2595,7 +2595,7 @@ snapshots:
 
   '@eslint/js@8.57.1': {}
 
-  '@eslint/js@9.25.0': {}
+  '@eslint/js@9.24.0': {}
 
   '@humanwhocodes/config-array@0.13.0':
     dependencies:
@@ -2851,13 +2851,13 @@ snapshots:
       chai: 5.2.0
       tinyrainbow: 2.0.0
 
-  '@vitest/mocker@3.1.1(vite@6.3.2(@types/node@22.14.1)(yaml@2.7.1))':
+  '@vitest/mocker@3.1.1(vite@6.3.1(@types/node@22.14.1)(yaml@2.7.1))':
     dependencies:
       '@vitest/spy': 3.1.1
       estree-walker: 3.0.3
       magic-string: 0.30.17
     optionalDependencies:
-      vite: 6.3.2(@types/node@22.14.1)(yaml@2.7.1)
+      vite: 6.3.1(@types/node@22.14.1)(yaml@2.7.1)
 
   '@vitest/pretty-format@3.1.1':
     dependencies:
@@ -4753,7 +4753,7 @@ snapshots:
       debug: 4.4.0
       es-module-lexer: 1.6.0
       pathe: 2.0.3
-      vite: 6.3.2(@types/node@22.14.1)(yaml@2.7.1)
+      vite: 6.3.1(@types/node@22.14.1)(yaml@2.7.1)
     transitivePeerDependencies:
       - '@types/node'
       - jiti
@@ -4768,7 +4768,7 @@ snapshots:
       - tsx
       - yaml
 
-  vite@6.3.2(@types/node@22.14.1)(yaml@2.7.1):
+  vite@6.3.1(@types/node@22.14.1)(yaml@2.7.1):
     dependencies:
       esbuild: 0.25.2
       fdir: 6.4.3(picomatch@4.0.2)
@@ -4784,7 +4784,7 @@ snapshots:
   vitest@3.1.1(@types/node@22.14.1)(yaml@2.7.1):
     dependencies:
       '@vitest/expect': 3.1.1
-      '@vitest/mocker': 3.1.1(vite@6.3.2(@types/node@22.14.1)(yaml@2.7.1))
+      '@vitest/mocker': 3.1.1(vite@6.3.1(@types/node@22.14.1)(yaml@2.7.1))
       '@vitest/pretty-format': 3.1.1
       '@vitest/runner': 3.1.1
       '@vitest/snapshot': 3.1.1
@@ -4800,7 +4800,7 @@ snapshots:
       tinyexec: 0.3.2
       tinypool: 1.0.2
       tinyrainbow: 2.0.0
-      vite: 6.3.2(@types/node@22.14.1)(yaml@2.7.1)
+      vite: 6.3.1(@types/node@22.14.1)(yaml@2.7.1)
       vite-node: 3.1.1(@types/node@22.14.1)(yaml@2.7.1)
       why-is-node-running: 2.3.0
     optionalDependencies:


Reviews:
user: login: tibo-openai
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-19T05:25:35Z
user: login: alphajoop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T09:41:28Z
user: login: alphajoop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T09:45:03Z

Review comments:
diff_hunk: @@ -40,7 +40,7 @@ jobs:
             ${{ runner.os }}-pnpm-store-
 
       - name: Install dependencies
-        run: pnpm install
+        run: pnpm install --no-frozen-lockfile
path: .github/workflows/ci.yml
user: login: tibo-openai
body: Why this change? We would like to fail fast if dependencies are stale
created_at: 2025-04-19T05:23:27Z
author_association: COLLABORATOR
diff_hunk: @@ -10,12 +10,14 @@
     "test": "pnpm --filter @openai/codex run test",
     "lint": "pnpm --filter @openai/codex run lint",
     "typecheck": "pnpm --filter @openai/codex run typecheck",
+    "changelog": "git-cliff --config cliff.toml --output CHANGELOG.ignore.md $LAST_RELEASE_TAG..HEAD",
     "prepare": "husky",
-    "changelog": "git-cliff --config cliff.toml --output CHANGELOG.ignore.md $LAST_RELEASE_TAG..HEAD"
+    "husky:add": "husky add"
path: package.json
user: login: tibo-openai
body: Why is this needed?
created_at: 2025-04-19T05:25:25Z
author_association: COLLABORATOR
diff_hunk: @@ -40,7 +40,7 @@ jobs:
             ${{ runner.os }}-pnpm-store-
 
       - name: Install dependencies
-        run: pnpm install
+        run: pnpm install --no-frozen-lockfile
path: .github/workflows/ci.yml
user: login: alphajoop
body: You're right, I'll fix that.
created_at: 2025-04-19T09:41:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,12 +10,14 @@
     "test": "pnpm --filter @openai/codex run test",
     "lint": "pnpm --filter @openai/codex run lint",
     "typecheck": "pnpm --filter @openai/codex run typecheck",
+    "changelog": "git-cliff --config cliff.toml --output CHANGELOG.ignore.md $LAST_RELEASE_TAG..HEAD",
     "prepare": "husky",
-    "changelog": "git-cliff --config cliff.toml --output CHANGELOG.ignore.md $LAST_RELEASE_TAG..HEAD"
+    "husky:add": "husky add"
path: package.json
user: login: alphajoop
body: This script is simply a shortcut for the husky add command, which allows you to add new Git hooks.
With Husky v9, the husky add command is used to create new hooks, but you can run it directly with npx husky add or pnpm exec husky add without needing a dedicated script in package.json.
created_at: 2025-04-19T09:45:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: tibo-openai
created_at: 2025-04-19T05:29:38Z
body: Thanks for the contrib (and the many others)! Left two comments to resolve.

One thing not related to the context of the change, but it would help me and others a little if the PR descriptions was a tad bit less verbose, although I do really appreciate the effort that goes into writing good PR descriptions! In this one for example, the title can be remove, most of the benefits section can be trimmed and it would be easier to skim if there was less use of formatting / bolding / titles.
author_association: COLLABORATOR
user: login: alphajoop
created_at: 2025-04-19T10:02:16Z
body: Thank you also for allowing us to put into practice the little knowledge we have. Don't worry about the descriptions, it took me a long time to translate them, I don't speak English. :)
author_association: CONTRIBUTOR
user: login: tibo-openai
created_at: 2025-04-19T14:18:49Z
body: ty!
author_association: COLLABORATOR