[{"data":1,"prerenderedAt":716},["ShallowReactive",2],{"blog-how-to/cursor-git-hooks-not-running":3},{"id":4,"title":5,"body":6,"category":682,"date":683,"dateModified":683,"description":684,"draft":685,"extension":686,"faq":687,"featured":685,"headerVariant":699,"image":700,"keywords":701,"meta":702,"navigation":196,"ogDescription":705,"ogTitle":700,"path":706,"readTime":707,"schemaOrg":708,"schemaType":709,"seo":710,"sitemap":711,"stem":712,"tags":713,"twitterCard":714,"__hash__":715},"blog/blog/how-to/cursor-git-hooks-not-running.md","Cursor Pre-Commit Hooks Not Running? The Source Control Panel Skips Them",{"type":7,"value":8,"toc":672},"minimark",[9,18,25,44,49,71,84,87,90,119,127,130,137,140,343,349,355,359,362,392,402,415,425,428,431,474,492,496,503,517,523,527,533,539,567,570,574,577,583,591,594,638,656,668],[10,11,12,13,17],"p",{},"If you wired up gitleaks or Husky to stop yourself committing a ",[14,15,16],"code",{},".env"," file, and you commit using the button in Cursor's Source Control panel, that hook has not been running since early August.",[10,19,20,21,24],{},"No error. No warning. No ",[14,22,23],{},"--no-verify"," anywhere in sight. The commit just succeeds, exactly as it would if the hook had run and passed.",[26,27,28],"tldr",{},[10,29,30,31,34,35,38,39,43],{},"Since Cursor 3.15.6 (2026-08-06), reporters found the bundled git extension sets ",[14,32,33],{},"core.hooksPath"," to the platform null device on git invocations from the Source Control panel, so commit-time hooks are skipped. Committing the same changes from the integrated terminal runs them normally. Cursor acknowledged it on 2026-08-12 and it was still reproducing on 3.17.21, the current stable release, as of 2026-08-28. Checking ",[14,36,37],{},"git config core.hooksPath"," in your own terminal will ",[40,41,42],"strong",{},"not"," reveal it.",[45,46,48],"h2",{"id":47},"what-was-reported-and-by-whom","What was reported, and by whom",[10,50,51,52,59,60,62,63,66,67,70],{},"The first report landed on the Cursor forum on ",[53,54,58],"a",{"href":55,"rel":56},"https://forum.cursor.com/t/git-hooks-silently-skipped-for-all-source-control-operations-since-3-15-6-git-extension-forces-core-hookspath-dev-null/167719",[57],"nofollow","2026-08-07",", against build 3.15.6 from the day before. The reporter decompiled the bundled git extension and found it setting ",[14,61,33],{}," to the platform null device (",[14,64,65],{},"/dev/null"," on macOS and Linux, ",[14,68,69],{},"\\\\.\\nul"," on Windows) on the git subprocess.",[10,72,73,74,79,80,83],{},"More than a dozen people have since reported or reproduced it on that thread, across macOS, Windows and WSL2. A ",[53,75,78],{"href":76,"rel":77},"https://forum.cursor.com/t/cursor-3-15-6-silently-disables-all-git-hooks-for-commits-made-from-the-source-control-panel-injects-core-hookspath-dev-null/167914",[57],"second thread"," was opened independently. The strongest corroboration isn't the headcount, though, since most people posted after reading the root-cause analysis. It's that one reporter confirmed it on Windows using git's own ",[14,81,82],{},"trace2"," instrumentation, and another measured commit time dropping from 37 seconds to 32 milliseconds, which is what a skipped hook looks like on the clock.",[10,85,86],{},"Two Cursor staff replied on 2026-08-10 and 2026-08-12, saying \"what you're seeing isn't intended\" and that the team was tracking it. Worth stating plainly: this reads as a regression, not a decision, and the vendor said so quickly.",[10,88,89],{},"As of 2026-08-28 it is still open. The thread carries confirmations against 3.16.29, 3.17.8 and 3.17.21, no changelog entry mentions hooks or the git extension, and 3.17.21 is what Cursor's download endpoint serves as current stable today.",[91,92,93],"info-box",{},[10,94,95,98,99,102,103,106,107,110,111,114,115,118],{},[40,96,97],{},"Scope, stated honestly."," What people have demonstrated is the ",[40,100,101],{},"commit"," path, so ",[14,104,105],{},"pre-commit",", ",[14,108,109],{},"commit-msg"," and ",[14,112,113],{},"prepare-commit-msg",". The mechanism would plausibly reach ",[14,116,117],{},"pre-push"," and other hooks too, but nobody has published a test of that, so treat wider impact as likely rather than established.",[45,120,122,123,126],{"id":121},"why-git-config-will-tell-you-nothing","Why ",[14,124,125],{},"git config"," will tell you nothing",[10,128,129],{},"Here's the part that turns an annoying bug into a genuinely dangerous one, and it's why the obvious check is worse than useless.",[10,131,132,133,136],{},"The setting doesn't arrive through a config file. Git reads config from environment variables when ",[14,134,135],{},"GIT_CONFIG_COUNT"," is set, and that is how the value reaches the subprocess. It exists only inside the process the editor spawned. Your terminal is a different process, so it never sees it.",[10,138,139],{},"We reproduced that on a scratch repo with plain git 2.43.0, no Cursor involved:",[141,142,144],"code-block",{"label":143},"The value is real inside the spawned process and invisible outside it",[145,146,151],"pre",{"className":147,"code":148,"language":149,"meta":150,"style":150},"language-bash shiki shiki-themes github-dark","$ git commit -q -m one\nPRE-COMMIT HOOK RAN\n\n$ env GIT_CONFIG_COUNT=1 \\\n      GIT_CONFIG_KEY_0=core.hooksPath \\\n      GIT_CONFIG_VALUE_0=/dev/null \\\n      git commit -q -m two\n                        # no output. the hook did not run.\n\n# inside that environment, git reports the value:\n$ env GIT_CONFIG_COUNT=1 ... git config core.hooksPath\n/dev/null\n\n# in your own terminal, which is what you would actually type:\n$ git config core.hooksPath\n                        # empty\n$ git config --show-origin --get core.hooksPath\n                        # nothing, in any config file\n","bash","",[14,152,153,179,191,198,215,223,231,246,253,258,264,286,292,297,303,314,320,337],{"__ignoreMap":150},[154,155,158,162,166,169,173,176],"span",{"class":156,"line":157},"line",1,[154,159,161],{"class":160},"svObZ","$",[154,163,165],{"class":164},"sU2Wk"," git",[154,167,168],{"class":164}," commit",[154,170,172],{"class":171},"sDLfK"," -q",[154,174,175],{"class":171}," -m",[154,177,178],{"class":164}," one\n",[154,180,182,185,188],{"class":156,"line":181},2,[154,183,184],{"class":160},"PRE-COMMIT",[154,186,187],{"class":164}," HOOK",[154,189,190],{"class":164}," RAN\n",[154,192,194],{"class":156,"line":193},3,[154,195,197],{"emptyLinePlaceholder":196},true,"\n",[154,199,201,203,206,209,212],{"class":156,"line":200},4,[154,202,161],{"class":160},[154,204,205],{"class":164}," env",[154,207,208],{"class":164}," GIT_CONFIG_COUNT=",[154,210,211],{"class":171},"1",[154,213,214],{"class":171}," \\\n",[154,216,218,221],{"class":156,"line":217},5,[154,219,220],{"class":164},"      GIT_CONFIG_KEY_0=core.hooksPath",[154,222,214],{"class":171},[154,224,226,229],{"class":156,"line":225},6,[154,227,228],{"class":164},"      GIT_CONFIG_VALUE_0=/dev/null",[154,230,214],{"class":171},[154,232,234,237,239,241,243],{"class":156,"line":233},7,[154,235,236],{"class":164},"      git",[154,238,168],{"class":164},[154,240,172],{"class":171},[154,242,175],{"class":171},[154,244,245],{"class":164}," two\n",[154,247,249],{"class":156,"line":248},8,[154,250,252],{"class":251},"sAwPA","                        # no output. the hook did not run.\n",[154,254,256],{"class":156,"line":255},9,[154,257,197],{"emptyLinePlaceholder":196},[154,259,261],{"class":156,"line":260},10,[154,262,263],{"class":251},"# inside that environment, git reports the value:\n",[154,265,267,269,271,273,275,278,280,283],{"class":156,"line":266},11,[154,268,161],{"class":160},[154,270,205],{"class":164},[154,272,208],{"class":164},[154,274,211],{"class":171},[154,276,277],{"class":164}," ...",[154,279,165],{"class":164},[154,281,282],{"class":164}," config",[154,284,285],{"class":164}," core.hooksPath\n",[154,287,289],{"class":156,"line":288},12,[154,290,291],{"class":160},"/dev/null\n",[154,293,295],{"class":156,"line":294},13,[154,296,197],{"emptyLinePlaceholder":196},[154,298,300],{"class":156,"line":299},14,[154,301,302],{"class":251},"# in your own terminal, which is what you would actually type:\n",[154,304,306,308,310,312],{"class":156,"line":305},15,[154,307,161],{"class":160},[154,309,165],{"class":164},[154,311,282],{"class":164},[154,313,285],{"class":164},[154,315,317],{"class":156,"line":316},16,[154,318,319],{"class":251},"                        # empty\n",[154,321,323,325,327,329,332,335],{"class":156,"line":322},17,[154,324,161],{"class":160},[154,326,165],{"class":164},[154,328,282],{"class":164},[154,330,331],{"class":171}," --show-origin",[154,333,334],{"class":171}," --get",[154,336,285],{"class":164},[154,338,340],{"class":156,"line":339},18,[154,341,342],{"class":251},"                        # nothing, in any config file\n",[10,344,345,346,348],{},"So a developer who suspects something is wrong, runs ",[14,347,37],{},", and sees an empty result will reasonably conclude their hooks are fine. They aren't. The check and the bug never intersect, because the check runs in the one process where the value was never set.",[350,351,352],"danger-box",{},[10,353,354],{},"Treat \"my git config looks clean\" as no evidence at all here. The only thing that proves a hook ran is the hook leaving something behind.",[45,356,358],{"id":357},"the-check-that-actually-works","The check that actually works",[10,360,361],{},"Make the hook prove itself. Add one line to it, commit both ways, and compare.",[363,364,365,375],"step",{"number":211},[10,366,367,368,371,372,374],{},"Add an evidence line to your existing ",[14,369,370],{},".git/hooks/pre-commit"," (or your Husky ",[14,373,105],{},"):",[145,376,378],{"className":147,"code":377,"language":149,"meta":150,"style":150},"date >> /tmp/hook-ran.log\n",[14,379,380],{"__ignoreMap":150},[154,381,382,385,389],{"class":156,"line":157},[154,383,384],{"class":160},"date",[154,386,388],{"class":387},"snl16"," >>",[154,390,391],{"class":164}," /tmp/hook-ran.log\n",[363,393,395],{"number":394},"2",[10,396,397,398,401],{},"Stage a trivial change and commit it using the ",[40,399,400],{},"Source Control panel button",".",[363,403,405],{"number":404},"3",[10,406,407,408,411,412,401],{},"Stage another trivial change and commit it from the ",[40,409,410],{},"integrated terminal"," with ",[14,413,414],{},"git commit -m test",[363,416,418],{"number":417},"4",[10,419,420,421,424],{},"Run ",[14,422,423],{},"cat /tmp/hook-ran.log",". Two entries means your hooks are running everywhere. One entry means the panel skipped it, and the terminal is the only path currently enforcing your checks.",[10,426,427],{},"Remove the evidence line afterwards.",[10,429,430],{},"If you want the mechanism rather than the symptom, git will show you its own config resolution:",[141,432,434],{"label":433},"Ask git where a setting came from",[145,435,437],{"className":147,"code":436,"language":149,"meta":150,"style":150},"GIT_TRACE2_CONFIG_PARAMS=\"core.*\" GIT_TRACE2_EVENT=\"$PWD/t2.log\" git commit -m test\n",[14,438,439],{"__ignoreMap":150},[154,440,441,445,448,451,454,456,459,462,465,467,469,471],{"class":156,"line":157},[154,442,444],{"class":443},"s95oV","GIT_TRACE2_CONFIG_PARAMS",[154,446,447],{"class":387},"=",[154,449,450],{"class":164},"\"core.*\"",[154,452,453],{"class":443}," GIT_TRACE2_EVENT",[154,455,447],{"class":387},[154,457,458],{"class":164},"\"",[154,460,461],{"class":443},"$PWD",[154,463,464],{"class":164},"/t2.log\"",[154,466,165],{"class":160},[154,468,168],{"class":164},[154,470,175],{"class":171},[154,472,473],{"class":164}," test\n",[10,475,476,477,480,481,484,485,488,489,401],{},"Plain ",[14,478,479],{},"GIT_TRACE2_EVENT"," on its own logs nothing useful here. The ",[14,482,483],{},"CONFIG_PARAMS"," filter is what makes git emit a ",[14,486,487],{},"def_param"," line naming the key, the value, and a scope of ",[14,490,491],{},"command",[45,493,495],{"id":494},"the-hooks-are-not-the-only-thing-switched-off","The hooks are not the only thing switched off",[10,497,498,499,502],{},"Buried in the same reports is a second key that has drawn almost no attention: ",[14,500,501],{},"core.attributesFile"," is pointed at the null device too.",[10,504,505,506,509,510,513,514,516],{},"That disables your ",[14,507,508],{},".gitattributes",". Line-ending normalization, diff drivers, merge drivers, and any ",[14,511,512],{},"filter"," you configured all stop applying to commits made through the panel. For a team that uses ",[14,515,508],{}," to keep CRLF out of the repository, or to run a cleaning filter over files before they're stored, this is a correctness problem that will show up later as a confusing diff rather than as an error now.",[10,518,519,520,522],{},"Nobody has written this one up. If your repository leans on ",[14,521,508],{},", it's worth the same marker-file treatment.",[45,524,526],{"id":525},"what-to-do-about-it","What to do about it",[10,528,529,532],{},[40,530,531],{},"Commit from the terminal for now."," It's the workaround the thread converged on and it needs no configuration change.",[10,534,535,538],{},[40,536,537],{},"Then check what already got through."," If this has been live on your machine since early August, the useful question isn't whether the hook works today. It's whether anything slipped past it in the last three weeks. Scan your history rather than your working tree, because a secret that was committed and then deleted is still in the history:",[141,540,542],{"label":541},"Scan the full history, not the current files",[145,543,545],{"className":147,"code":544,"language":149,"meta":150,"style":150},"gitleaks detect --source . --log-opts=\"--all\"\n",[14,546,547],{"__ignoreMap":150},[154,548,549,552,555,558,561,564],{"class":156,"line":157},[154,550,551],{"class":160},"gitleaks",[154,553,554],{"class":164}," detect",[154,556,557],{"class":171}," --source",[154,559,560],{"class":164}," .",[154,562,563],{"class":171}," --log-opts=",[154,565,566],{"class":164},"\"--all\"\n",[10,568,569],{},"If that turns something up, rotate the credential first and clean the history second. Rotating second leaves the key live while you rewrite commits.",[45,571,573],{"id":572},"the-wider-point-which-outlasts-this-bug","The wider point, which outlasts this bug",[10,575,576],{},"Cursor will fix this. When they do, the specific advice above stops mattering and the lesson underneath it does not.",[10,578,579,580,582],{},"A pre-commit hook is not a security control. It's a convenience that runs only if whatever invoked git decided to run it, and you generally don't control that decision. A GUI button, a CI runner, a cloud agent, a teammate typing ",[14,581,23],{}," because the hook was slow: every one of them is a path around it, and none of them announces itself.",[10,584,585,586,590],{},"We say this on our own pages that ",[53,587,589],{"href":588},"/blog/how-to/secret-scanning","recommend setting up secret scanning hooks",", and it's worth repeating with a live example attached. Hooks belong in the stack as the fast first layer that catches your own mistakes early. The layer you actually rely on is the one that runs somewhere you don't control: scanning the repository, and scanning the deployed app.",[10,592,593],{},"This bug is a good argument for that structure, because it removed the first layer for three weeks without telling anybody.",[595,596,597,607,620,626,632],"faq-section",{},[598,599,601],"faq-item",{"question":600},"Why are my pre-commit hooks not running in Cursor?",[10,602,603,604,606],{},"If you committed from the Source Control panel rather than the terminal, reporters found the bundled git extension points ",[14,605,33],{}," at the null device for that invocation, so git finds no commit-time hooks. Reported 2026-08-07 against 3.15.6, acknowledged by Cursor on 2026-08-12, still reproducing on 3.17.21 as of 2026-08-28.",[598,608,610],{"question":609},"Does git config core.hooksPath tell me if I am affected?",[10,611,612,613,615,616,619],{},"No, and that's the trap. The value arrives as environment variables on the process the editor spawned, not in any config file, so in your terminal ",[14,614,37],{}," comes back empty and ",[14,617,618],{},"--show-origin"," finds nothing. A clean result there is not evidence your hooks ran.",[598,621,623],{"question":622},"How do I actually check whether my hooks ran?",[10,624,625],{},"Make the hook leave evidence. Append a line to a file from inside the hook, commit once from the panel and once from the terminal, then compare. The missing entry tells you which path skipped it.",[598,627,629],{"question":628},"What is the workaround?",[10,630,631],{},"Commit from the integrated terminal. The same staged changes run hooks normally there. Beyond that, stop treating a local hook as the thing standing between you and a leaked key.",[598,633,635],{"question":634},"Does this mean secrets I committed are exposed?",[10,636,637],{},"It means a commit your hook would have blocked may have gone through. Whether anything sensitive is genuinely in your history is a separate question, and you answer it by scanning the history directly rather than trusting the hook.",[639,640,641,646,651],"related-articles",{},[642,643],"related-card",{"description":644,"href":588,"title":645},"The three-layer setup this bug removes one layer from, and why the other two matter.","How to Enable Secret Scanning",[642,647],{"description":648,"href":649,"title":650},"What to run once you learn a commit got through without being checked.","/blog/how-to/detect-secrets-in-git","Find Secrets Already in Your Git History",[642,652],{"description":653,"href":654,"title":655},"Rotate first, clean history second. The order matters more than people expect.","/blog/how-to/fix-cursor-api-key-exposure","Fix an Exposed API Key in a Cursor Project",[657,658,661,665],"cta-box",{"href":659,"label":660},"/","Start Free Scan",[45,662,664],{"id":663},"dont-rely-on-a-hook-you-cant-see-run","Don't Rely on a Hook You Can't See Run",[10,666,667],{},"CheckYourVibe scans what your deployed app actually ships, which is the layer no editor setting can silently switch off.",[669,670,671],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}",{"title":150,"searchDepth":181,"depth":181,"links":673},[674,675,677,678,679,680,681],{"id":47,"depth":181,"text":48},{"id":121,"depth":181,"text":676},"Why git config will tell you nothing",{"id":357,"depth":181,"text":358},{"id":494,"depth":181,"text":495},{"id":525,"depth":181,"text":526},{"id":572,"depth":181,"text":573},{"id":663,"depth":181,"text":664},"how-to","2026-08-28","Since Cursor 3.15.6, the Source Control panel silently skips commit-time hooks. Your gitleaks and Husky checks never run, and git config will not show it.",false,"md",[688,690,693,695,697],{"question":600,"answer":689},"If you committed using the Source Control panel rather than the terminal, reporters found the bundled git extension sets core.hooksPath to the null device on that invocation, so git finds no commit-time hooks to run. Reported 2026-08-07 against Cursor 3.15.6, acknowledged by Cursor staff on 2026-08-12, and still reproducing on 3.17.21 as of 2026-08-28.",{"question":691,"answer":692},"Does running git config core.hooksPath tell me if I am affected?","No, and this is the trap. The value reaches git through environment variables on the process the editor spawned, not through any config file, so in your own terminal git config core.hooksPath returns empty and git config --show-origin finds nothing. A clean result there does not mean your hooks ran.",{"question":622,"answer":694},"Make the hook leave evidence. Add a line to your pre-commit hook that appends to a file, commit once from the Source Control panel and once from the terminal, then compare. The commit whose evidence is missing is the one that skipped the hook.",{"question":628,"answer":696},"Commit from the integrated terminal with git commit. The same staged changes run hooks normally that way. Longer term, do not rely on a local hook as your only check, because the hook only runs if whatever invoked git decided to run it.",{"question":634,"answer":698},"It means a commit that your gitleaks or detect-secrets hook would have blocked may have gone through. Whether anything sensitive is actually in your history is a separate question. Scan the repository history directly rather than assuming the hook covered you.","yellow",null,"cursor pre-commit hook not running, husky not running in cursor, cursor core.hookspath dev null, cursor git hooks skipped, gitleaks not running cursor, cursor source control hooks",{"trendTrigger":703,"trendDate":704},"Cursor 3.15.6 git extension sets core.hooksPath to the null device on Source Control git invocations, silently skipping commit-time hooks","2026-08-06","Commit from Cursor's Source Control button and your pre-commit hooks are skipped with no error. Acknowledged by Cursor on 2026-08-12, still open on 3.17.21.","/blog/how-to/cursor-git-hooks-not-running","7 min read","[object Object]","TechArticle",{"title":5,"description":684},{"loc":706},"blog/how-to/cursor-git-hooks-not-running",[],"summary_large_image","GufalCMGD9NVr0BMgCzAIEvYf2aEq3WnbQYr_LhxVIU",1789672859757]