@@ -64,8 +64,15 @@ func runHookInstall(cmd *cobra.Command, args []string) error {
6464}
6565
6666func runHookRun (cmd * cobra.Command , args []string ) error {
67- // Get staged files for the commit
68- output , err := exec .Command ("git" , "diff" , "--cached" , "--name-only" ).Output ()
67+
68+ // Allow skipping via ENVEIL_SKIP=1 environment variable
69+ if os .Getenv ("ENVEIL_SKIP" ) == "1" {
70+ fmt .Fprintln (os .Stderr , " Enveil: scan skipped (ENVEIL_SKIP=1)" )
71+ return nil
72+ }
73+
74+ // Get only added or modified staged files, not deletions
75+ output , err := exec .Command ("git" , "diff" , "--cached" , "--name-only" , "--diff-filter=ACM" ).Output ()
6976 if err != nil {
7077 return fmt .Errorf ("error getting staged files: %w" , err )
7178 }
@@ -82,6 +89,15 @@ func runHookRun(cmd *cobra.Command, args []string) error {
8289 continue
8390 }
8491
92+ // Check if the file is a blocked .env file
93+ if isBlockedEnvFile (file ) {
94+ totalFindings ++
95+ fmt .Fprintf (os .Stderr , "\n Enveil: blocked file detected: %s\n " , file )
96+ fmt .Fprintf (os .Stderr , " This file should not be committed directly.\n " )
97+ fmt .Fprintf (os .Stderr , " Use 'enveil import %s' to import it into the vault instead.\n " , file )
98+ continue
99+ }
100+
85101 // Skip binary files and files that do not make sense to scan
86102 if isBinaryExtension (file ) || isIgnoredFile (file ) {
87103 continue
@@ -106,14 +122,57 @@ func runHookRun(cmd *cobra.Command, args []string) error {
106122 }
107123
108124 if totalFindings > 0 {
109- fmt .Fprintf (os .Stderr , "\n Commit blocked. Move secrets to vault with 'enveil set'\n " )
110- fmt .Fprintf (os .Stderr , " If you are sure it is not a secret: git commit --no-verify\n \n " )
125+ fmt .Fprintf (os .Stderr , "\n Commit blocked by Enveil.\n " )
126+ fmt .Fprintf (os .Stderr , " To fix: move secrets to vault with 'enveil set' or 'enveil import'\n " )
127+ fmt .Fprintf (os .Stderr , "\n To override (only if you know what you are doing):\n " )
128+ fmt .Fprintf (os .Stderr , " ENVEIL_SKIP=1 git commit\n " )
129+ fmt .Fprintf (os .Stderr , " git commit --no-verify\n \n " )
111130 os .Exit (1 )
112131 }
113132
114133 return nil
115134}
116135
136+ // isBlockedEnvFile returns true for .env files that should never be committed
137+ func isBlockedEnvFile (filename string ) bool {
138+ base := filepath .Base (filename )
139+
140+ // Always allow these patterns - they are templates with fake values
141+ allowed := []string {
142+ ".env.example" ,
143+ ".env.template" ,
144+ ".env.sample" ,
145+ ".env.test" ,
146+ ".env.development" ,
147+ ".env.staging" ,
148+ }
149+ for _ , a := range allowed {
150+ if base == a {
151+ return false
152+ }
153+ }
154+
155+ // Block these patterns - they contain real secrets
156+ blocked := []string {
157+ ".env" ,
158+ ".env.local" ,
159+ ".env.production" ,
160+ ".env.prod" ,
161+ }
162+ for _ , b := range blocked {
163+ if base == b {
164+ return true
165+ }
166+ }
167+
168+ // Block any .env.*.local pattern
169+ if strings .HasPrefix (base , ".env." ) && strings .HasSuffix (base , ".local" ) {
170+ return true
171+ }
172+
173+ return false
174+ }
175+
117176// isIgnoredFile returns true for files that are known to contain high entropy non-secret content
118177func isIgnoredFile (filename string ) bool {
119178 ignored := map [string ]bool {
0 commit comments