env-sync
Bir commit, .env.example dosyasında olmayan env variable'ları okuduğunda bunu modele söyleyen bir Claude Code Mod'u. Modelin çalıştırdığı her git commit sonrasında mod, commit'in eklediği satırları okur ve referans dosyanın listelemediği variable'ları commit'in sonucuna ekler. Commit hiç durdurulmaz.
Ne yapar
Mod Bash tool'unu hook'lar.
git commitçalıştıran bir komut kontrol edilir (git -C <dir> commitde,--dry-runve--helpdeğil).Komut çalışmadan önce repository root'unu bulur: session'ın dizini, commit'ten önceki son
cdve commit'in kendigit -Cdeğeri. SonraHEAD'i kaydeder.HEAD'i ilerleten başarılı bir komuttan sonra root'taki ilk referans dosyayı okur:.env.example, yoksa.env.sample, yoksa.env.dist. Bunlardan hiçbiri olmayan bir repository hiçbir şey almaz.Eklenen satırları
git show --format= --unified=0 HEADile okur ve şu env okumalarını bulur:Dil Okuma JavaScript, TypeScript process.env.X,process.env['X'],import.meta.env.XPython os.getenv('X'),os.environ['X'],os.environ.get('X'),getenv('X')Go os.Getenv("X"),os.LookupEnv("X")PHP, Laravel env('X'),getenv('X'),$_ENV['X'],$_SERVER['X']Rust std::env::var("X"),env::var("X"),env::var_os("X")Ruby ENV['X'],ENV.fetch('X')Java, Kotlin System.getenv("X")Ad büyük harftir (
[A-Z][A-Z0-9_]*).NODE_ENV,HOME,PATH,USER,PWD,SHELL,TMPDIR,TERM,LANGveCIatlanır;$_SERVERiçindeki request değerleri de (HTTP_*,REQUEST_*,SERVER_*ve benzerleri). Düz metin dosyalarının satırları (.md,.txt,.rstve benzerleri) okunmaz.Bir variable, referans dosyada
X=,export X=ya da comment'lenmiş# X=satırı varsa listelenmiş sayılır. Model, commit'in sonucundan sonra şu notu okur:env-sync: this commit reads env variables .env.example lacks: STRIPE_KEY (src/pay.ts:12) · REDIS_URL (app/cache.py:4). Add them to .env.example with a placeholder value, never a real secret.Her variable bir kere, ilk eklendiği satırda adlandırılır. En fazla 10 tanesi adlandırılır, gerisi sayılır.
Aynı anda transcript'e bir satır yazılır, böylece modele ne söylendiğini görürsünüz. Bu satır talimat cümlesi olmadan yalnız variable'ları taşır:
env-sync: env variables .env.example lacks: STRIPE_KEY (src/pay.ts:12) · REDIS_URL (app/cache.py:4)Not ve satır ayrı iki kanaldır: model satırı hiç okumaz, siz notu hiç okumazsınız.
sidebar açıkken bu variable'lar oraya gider, variable başına bir satır, stream'in içinde bir kayıt olarak; transcript temiz kalır. Kayıt, yenileri onu pane'in dışına itene kadar durur. Sidebar kapalıyken ya da o mod kurulu değilken yukarıdaki transcript satırı yazılır.
Bulgu asla hatırlanan bir cevap değildir. Her ölçüm, sonraki her commit'ten sonra ve guarded bir git komutundan önce, iki kaynağı da tekrar okur, yani iki yoldan kapanır:
- referans dosya variable'ı listeler;
- eklenen satırları onu okuyan dosya artık okumuyordur, çünkü kod değişti ya da geri alındı. Artık var olmayan bir dosya da hiçbir şey okumaz.
Çözülen bir variable bulgudan hemen çıkar ve geriye bir şey kalmadığında kayıt temizlenir:
env-sync: .env.example now lists the variables it lacked: STRIPE_KEY · REDIS_URL env-sync: the code no longer reads: STRIPE_KEYSidebar kapalıyken aynı metin tek bir transcript satırıdır. Model bunların hiçbirini okumaz: bulgu kendi işiyle kapanmıştır, yani bir not yalnız az önce yaptığını tekrarlardı. Duran ama okunamayan bir dosya variable'ını açık tutar, çünkü okunamayan bir dosya hiçbir şeyi kanıtlamaz.
Modelin kapatmadığı bir bulgu her main-loop turn sonunda tekrar ölçülür ve geriye kalan, bir sonraki prompt ile modele tek not olarak ulaşır:
env-sync: .env.example still lacks 1 env variable(s) the code reads: STRIPE_KEY (src/pay.ts). Add them to .env.example with a placeholder value, or take the reads out.Turn başına bir not, prompt başına değil. Bu olmasa bulgu bir kere, commit anında söylenir ve model onu unutmuşken pane'de dururdu. Siz yeni bir şey okumazsınız: pane zaten aynı bulguyu taşır.
denymodunda mod ayrıca, bir bulgu açıkkengit commit,git pushvegit mergekomutlarını durdurur. Bir komutu durdurmadan önce iki kaynağı da tekrar ölçer, yani variable'ları ekleyen bir commit de, okumaları kaldıran bir commit de gate'i kendisi açar. Birgit commityalnız kendi dosyalarından sorumludur: mod index'i okur (git diff --cached --name-only) ve commit eksik variable'ları okuyan dosyalardan hiçbirini tutmuyorsa çalışmasına izin verir, kaç bulgunun durduğunu söyleyen bir satırla.pushvemergehiçbir index okumaz, bu yüzden orada her bulgu durur. Kaçış yolu yoktur; gate'i yalnız kişi/env-sync mode noteile kapatır.notevarsayılandır ve hiçbir şeyi durdurmaz.
Bir git hatası bir kere log'lanır ve commit'in sonucu olduğu gibi kalır.
Canlı testte model, .env.example dosyası yalnız DB_URL listeleyen bir repository'nin bir dosyasına process.env.STRIPE_KEY ekledi, commit etti ve notu kelimesi kelimesine aktardı.
Komut
/env-sync on ya da off, mod ve hâlâ eksik variable'lar
/env-sync on | off varsayılan on
/env-sync mode note sadece not; varsayılan
/env-sync mode deny bir variable eksikken commit, push ve merge de durur
Kurulum
claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install env-sync@kilimcininkoroglu-mods
Function hook'lar early access. Flag olmadan hiçbir şey yüklenmez. Flag'i kalıcı yapmak için ~/.claude/settings.json dosyasına ekleyin:
{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }
Kurulumdan sonra
- Claude Code'u yeniden başlatın.
Nereye uzanır
Claude Code 2.1.278 üzerinde claude plugin validate ile doğrulandı:
❯ ./register.ts hooks: session.start, command.run{command=env-sync}, turn.complete, prompt.submit, tool.call{tool=Bash}
❯ ./register.ts calls: $.command.register, $.fs.exists (via referenceFile, stillRead), $.fs.read (via commitNote, gate, recheckNow, stillRead), $.process.run (via git, scopeOf), $.session.cwd (via beforeCommit, recheckNow), $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get, $.store.set (via runCommand, setMode), $.ui.log (via denyFor, report, toPerson)
Reach L2, process çalıştırır.
1. Okur: Bash komut metnini; repository root'undaki referans dosyayı; açık bir bulgunun geldiği her dosyayı tekrar, turn sonunda da; git üzerinden commit'in eklediği satırları
2. Çalıştırır: git rev-parse, git show ve git diff --cached --name-only, salt okuma, argv ile, commit başına en fazla dört kere; bulgu dururken turn sonunda git rev-parse
3. Gönderir: commit'in sonucundan sonra modele bir not, bulgu dururken bir sonraki prompt ile bir not daha, ve transcript'e bir satır; makineden hiçbir şey çıkmaz
4. Saklar: $.store içinde on/off ayarını ve modu
5. Düşman girdi: dizin komut metninden gelir ve git'e yalnız working directory olarak ulaşır, hiçbir zaman shell üzerinden geçmez; not variable adlarını adlandırır, .env.example içindeki bir değeri asla
Sınırlar
- Bir config katmanı üzerinden okunan variable (Laravel
config('x'), bir settings sınıfı,dotenvşema dosyaları) görülmez; çalışma zamanında kurulan bir ad da görülmez (process.env[name]). - Yalnız repository root'undaki referans dosya okunur. Kendi
.env.exampledosyası olan bir monorepo paketi, root'taki dosyaya göre kontrol edilir. git commitkomutunu gizleyen bir script ya da alias üzerinden atılan commit görülmez.cd ~/xgenişletilmez.- Bir merge commit'inin birleşik diff'i okunmaz.
denymodunun kaçış yolu yoktur. Bulgu düzeltilemiyorsa kişi gate'i/env-sync mode noteile kapatır.- Gate komutu metin olarak okur, yani bir script ya da alias üzerinden atılan commit gate'ten geçer.
git commit -a,-amve--sonrası pathspec taşıyan bir commit index'e göre daraltılmaz, çünkü bunlar index'in henüz tutmadığı dosyaları commit eder. Onlar için her açık bulgu durur.- Bulgu, commit'in variable'ı okuduğu dosyaya göre ölçülür. Başka bir dosyaya taşınan bir okuma orada gitmiş sayılır ve onu başka yere ekleyen commit bulguyu tekrar raporlar.
Geliştirme
make install # eslint, typescript-eslint, typescript
make lint # complexity limiti 10, üstünde build'i düşürür
make typecheck # /plugin-types ile üretilen .claude/types/ gerekir
make validate
make test # claude plugin test
env-sync
A Claude Code Mod that tells the model when a commit reads env variables that .env.example lacks. After each git commit the model runs, the mod reads the lines the commit added, and adds the variables the reference file does not list to the commit's result. The commit is never stopped.
What it does
The mod hooks the Bash tool. A command that runs
git commit(alsogit -C <dir> commit, not--dry-runor--help) is checked.Before the command runs, it finds the repository root from the session's directory, the last
cdbefore the commit and the commit'sgit -C, and recordsHEAD.After a successful command that moved
HEAD, it reads the first reference file at the root:.env.example, else.env.sample, else.env.dist. A repository without one gets nothing.It reads the added lines with
git show --format= --unified=0 HEADand finds these env reads:Language Reads JavaScript, TypeScript process.env.X,process.env['X'],import.meta.env.XPython os.getenv('X'),os.environ['X'],os.environ.get('X'),getenv('X')Go os.Getenv("X"),os.LookupEnv("X")PHP, Laravel env('X'),getenv('X'),$_ENV['X'],$_SERVER['X']Rust std::env::var("X"),env::var("X"),env::var_os("X")Ruby ENV['X'],ENV.fetch('X')Java, Kotlin System.getenv("X")A name is upper case (
[A-Z][A-Z0-9_]*).NODE_ENV,HOME,PATH,USER,PWD,SHELL,TMPDIR,TERM,LANGandCIare skipped, and so are the request values of$_SERVER(HTTP_*,REQUEST_*,SERVER_*and the like). Lines of prose files (.md,.txt,.rstand the like) are not read.A variable counts as listed when the reference file has
X=,export X=or a commented# X=line. The model reads this note after the commit's result:env-sync: this commit reads env variables .env.example lacks: STRIPE_KEY (src/pay.ts:12) · REDIS_URL (app/cache.py:4). Add them to .env.example with a placeholder value, never a real secret.Each variable is named once, at its first added line. At most 10 are named, the rest counted.
The same moment writes one line to the transcript, so you see what the model was told. The line holds the variables alone, without the instruction:
env-sync: env variables .env.example lacks: STRIPE_KEY (src/pay.ts:12) · REDIS_URL (app/cache.py:4)The note and the line are separate channels: the model never reads the line, and you never read the note.
While the sidebar is open, those variables go there instead, one line per variable, as an entry in its stream, and the transcript stays clean. The entry stays until newer ones push it off the pane. With the sidebar closed, or without that mod installed, the transcript line is written as above.
A finding is never a remembered answer. Each measure, after every later commit and before a guarded git command, reads both sources again, so it closes two ways:
- the reference file lists the variable;
- the file whose added lines read it does not read it any more, because the code was changed or reverted. A file that is gone reads nothing either.
A variable that settled leaves the finding at once, and the entry is cleared when nothing is left:
env-sync: .env.example now lists the variables it lacked: STRIPE_KEY · REDIS_URL env-sync: the code no longer reads: STRIPE_KEYWith the sidebar closed the same text is one transcript line. The model reads nothing of this: the finding closed by its own work, so a note would only repeat what it just did. A file that is there and cannot be read keeps its variable open, because an unread file proves nothing.
A finding the model did not close is measured again at the end of each main-loop turn, and what is left reaches the model as one note with its next prompt:
env-sync: .env.example still lacks 1 env variable(s) the code reads: STRIPE_KEY (src/pay.ts). Add them to .env.example with a placeholder value, or take the reads out.One note per turn, not one per prompt. Without this the finding would be said once, at the commit, and then stand in the pane while the model forgot it. You read nothing new: the pane already carries the same finding.
In
denymode the mod also stopsgit commit,git pushandgit mergewhile a finding is open. Before it stops one it measures both sources again, so a commit that added the variables, and one that took the reads out, each open the gate themselves. Agit commitanswers for its own files alone: the mod reads the index (git diff --cached --name-only) and lets the commit run when it holds none of the files that read the missing variables, with one line to you naming how many still stand. Apushand amergehold no index to read, so every finding stands there. There is no bypass; only the person turns the gate off with/env-sync mode note.notemode is the default and stops nothing.
A git error is logged once, and the commit's result stays as it was.
In the live check the model added process.env.STRIPE_KEY to a file of a repository whose .env.example listed only DB_URL, committed it, and quoted the note word for word.
Command
/env-sync on or off, the mode, and the variables still missing
/env-sync on | off on by default
/env-sync mode note note only; the default
/env-sync mode deny a commit, a push and a merge also stop while a variable is missing
Install
claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install env-sync@kilimcininkoroglu-mods
Function hooks are early access. Nothing loads without the flag. To keep it on, add this to ~/.claude/settings.json:
{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }
After installing
- Restart Claude Code.
What it can reach
Validated with claude plugin validate on Claude Code 2.1.278:
❯ ./register.ts hooks: session.start, command.run{command=env-sync}, turn.complete, prompt.submit, tool.call{tool=Bash}
❯ ./register.ts calls: $.command.register, $.fs.exists (via referenceFile, stillRead), $.fs.read (via commitNote, gate, recheckNow, stillRead), $.process.run (via git, scopeOf), $.session.cwd (via beforeCommit, recheckNow), $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get, $.store.set (via runCommand, setMode), $.ui.log (via denyFor, report, toPerson)
Reach L2, runs processes.
1. Reads: the Bash command text; the reference file at the repository root; each file an open finding came from, again, also at the turn's end; through git, the commit's added lines
2. Runs: git rev-parse, git show and git diff --cached --name-only, read-only, by argv, at most four times per commit, and git rev-parse at the turn's end while a finding stands
3. Sends: a note to the model after the commit's result, one more with the next prompt while a finding stands, and one line to the transcript; nothing leaves the machine
4. Persists: in $.store, the on/off setting and the mode
5. Hostile input: the directory comes from the command text and reaches git only as the working directory, never through a shell; the note names variables, never a value from .env.example
Limits
- A variable read through a config layer (Laravel
config('x'), a settings class,dotenvschema files) is not seen, and neither is a name built at run time (process.env[name]). - Only the reference file at the repository root is read. A monorepo package with its own
.env.exampleis checked against the root file. - A commit through a script or an alias that hides
git commitis not seen.cd ~/xis not expanded. - A merge commit's combined diff is not read.
- The
denymode has no bypass. When a finding cannot be fixed, the person turns the gate off with/env-sync mode note. - The gate reads the command as text, so a commit through a script or an alias passes it.
- A
git commit -a, a-amand a commit with a pathspec after--are not narrowed to the index, because they commit files the index does not hold yet. Every open finding stands for those. - A finding is measured against the file the commit read the variable in. A read moved to another file counts as gone there, and the commit that adds it elsewhere reports it again.
Development
make install # eslint, typescript-eslint, typescript
make lint # complexity limit 10, fails the build above it
make typecheck # needs .claude/types/ from /plugin-types
make validate
make test # claude plugin test