L2 v0.6.0 source

sql-concat-watch

Bir edit'in query parameter'ı geçmek yerine string'leri birleştirerek SQL kurduğunu modele söyleyen bir Claude Code Mod'u. Not Edit'in sonucuyla gelir ve her satırı adlandırır, yani model query'yi aynı turda yeniden yazar. Varsayılan olarak hiçbir şey durmaz; deny modunda bir dosya hâlâ SQL birleştirirken commit, push ve merge durur.

Ne yapar

  1. Mod Edit ve Write tool'larını hook'lar. Bir kaynak dosyada (.ts, .tsx, .js, .jsx, .mjs, .cjs, .py, .php, .go, .rb, .java, .kt, .cs, .rs) başarılı bir çağrıdan sonra edit'in eklediği satırları okur: new_string içinde olup old_string içinde olmayanları ya da bir Write'ın her satırını.

  2. Bir satır, SQL taşıyor ve içine bir değer birleştiriyorsa sayılır:

    SQL Birleştirme
    Büyük harfli SELECT … FROM, INSERT INTO, UPDATE … SET, DELETE FROM, WHERE, VALUES, ORDER BY, GROUP BY, JOIN "…" + x ve x + "…" (JS, Java, Go, C#, Kotlin)
    Herhangi bir yazımla select * ya da select a, b from, insert into t (, delete from t where, update t set a = Python f-string'leri, % ve .format(
    PHP "… $var" ve "…" . $var, Kotlin "… $var"
    C# $"… {x}", Ruby "… #{x}"
    fmt.Sprintf(, String.format(, format!(
    ${…} taşıyan bir template literal, birkaç satıra yayılanı da

    Değerleri parameter olarak geçen tag'li bir template literal dokunulmadan bırakılır: sql`…`, Prisma.sql`…`, prisma.$queryRaw`…`, $executeRaw`…`. $queryRawUnsafe böyle bir tag değildir. Yorum satırları ve İngilizce düz metin ("select a file from the list: " + name) sayılmaz.

  3. Model bu notu Edit'in sonucundan sonra okur:

    sql-concat-watch: this edit builds SQL from strings: src/db.ts:14 · src/db.ts:22. Pass values as query parameters (?, $1, :name) instead of joining them into the SQL text.
    

    Satır numarası edit sonrası dosyadan gelir; bir Write kendi içeriğinden numaralandırılır. En fazla 8 yer adlandırılır, kalanı sayılır. Dosya okunamadığında path satırsız durur ve hata bir kere log'lanır. Dosya session'ın başladığı dizinin içindeyse path o dizine göre yazılır. O dizin session'ın başlangıcında bir kere okunur, çünkü bir Bash cd session'ın kendi dizinini taşır.

  4. 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 yalnız yerleri taşır, talimat cümlesi olmadan:

    sql-concat-watch: SQL built from strings: src/db.ts:14 · src/db.ts:22
    

    Not ve satır ayrı iki kanaldır: model satırı hiç okumaz, siz notu hiç okumazsınız.

  5. sidebar açıkken bu yerler oraya gider, her biri bir satır olarak, stream'inde bir entry halinde, ve transcript temiz kalır. Entry, yenileri pane'den itene kadar durur. Sidebar kapalıyken ya da o mod kurulu değilken transcript satırı yukarıdaki gibi yazılır.

  6. Bir bulgu, dosya o satırları artık taşımayana kadar açık kalır. Sonraki bir Edit ya da Write'tan sonra mod her açık dosyayı yeniden okur ve satırlarının hepsi gitmiş bir dosya kapanır. Artık var olmayan bir dosya da kapanır, çünkü artık hiçbir satır taşımaz; var olan ama okunamayan bir dosya bulgusunu açık tutar, çünkü okunamayan bir dosya hiçbir şeyi kanıtlamaz:

    sql-concat-watch: the SQL built from strings is gone from src/db.ts: src/db.ts:14 · src/db.ts:22
    

    Sidebar kapalıyken aynı metin tek bir transcript satırıdır. Model bunun hiçbirini okumaz: query'yi kendisi yeniden yazdı.

  7. Modelin kapatmadığı bir bulgu her ana döngü turunun sonunda yeniden ölçülür ve kalan, bir sonraki prompt'la modele tek bir not olarak ulaşır:

    sql-concat-watch: 2 place(s) still build SQL from strings: src/db.ts:14 · src/db.ts:22. Pass the values as query parameters (?, $1, :name), or take the lines out.
    

    Tur başına bir not, prompt başına değil. Bu olmasa bulgu bir kere, edit anında söylenir ve sonra model onu unutmuşken pane'de dururdu. Siz yeni bir şey okumazsınız: pane zaten aynı bulguyu taşıyor.

  8. deny modunda mod ayrıca, bir dosya hâlâ string'lerden SQL kurarken git commit, git push ve git merge komutlarını durdurur. Bir komutu durdurmadan önce her açık dosyayı yeniden okur, yani modelin düzelttiği bir dosya gate'i kendisi açar. Bir git commit yalnız kendi dosyaları için cevap verir: mod index'i okur (git diff --cached --name-only) ve index açık dosyaların hiçbirini tutmuyorsa commit'in çalışmasına izin verir, size kaçının hâlâ durduğunu söyleyen bir satırla. Bir push ve bir merge okunacak index tutmaz, yani orada her bulgu durur. Kaçış yolu yok; gate'i yalnız kişi /sql-concat-watch mode note ile kapatır. note modu varsayılandır ve hiçbir şeyi durdurmaz.

Canlı kontrolde model bir dosyaya tek bir Edit ile db.query(`SELECT * FROM users WHERE id = ${id}`) koydu, src/users.ts:3 adlandıran notu okudu ve cevabında parameterized biçimi adlandırdı.

Komut

/sql-concat-watch                 on ya da off, mod ve hâlâ SQL birleştiren dosyalar
/sql-concat-watch on | off        varsayılan on
/sql-concat-watch mode note       yalnız not; varsayılan
/sql-concat-watch mode deny       bir dosya SQL birleştirirken commit, push ve merge de durur

Kurulum

claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install sql-concat-watch@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

  1. 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=sql-concat-watch}, turn.complete, prompt.submit, tool.call{tool=Bash}, tool.call{tool=Edit}, tool.call{tool=Write}
❯ ./register.ts calls: $.command.register, $.fs.exists (via isGone), $.fs.read (via fileText), $.process.run (via stagedPaths), $.session.cwd, $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get, $.store.set (via runCommand, setMode), $.ui.log (via fileText, gate, toPerson)

Reach L2, index'i okumak için git çalıştırır.

1. Okur:     her Edit ve Write çağrısının metnini; Bash komut metnini; SQL birleştiren bir Edit sonrası edit edilen dosyayı, satır numaraları için, ve bulgu dururken her açık dosyayı yeniden
2. Çalıştırır: deny modunda bir commit'te git rev-parse --show-toplevel ve git diff --cached --name-only, commit'in hangi dosyaları tuttuğunu okumak için
3. Gönderir: SQL birleştiren bir edit'ten sonra modele bir not, bulgu dururken sonraki prompt'la bir tane 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: edit edilen metin yalnız regular expression ile eşleştirilir ve file:line olarak yazılır, hiçbir zaman çalıştırılmaz

Sınırlar

  • Kontrol satır satır regular expression ile yapılır, bir parser ile değil. Birkaç ifadede bir değişkende kurulan bir query (q = "SELECT …"; q += id) görülmez.
  • Bir parameter'ın duramayacağı bir table ya da column adına birleştirilen bir değer de bildirilir. Not orada bir gerekçe ister, hiçbir şeyi durdurmaz.
  • Birleştirilmiş string taşıyan bir query builder çağrısı (knex.raw, DB::raw, whereRaw) yalnız string'in kendisi SQL keyword'leri taşıdığında görülür.
  • Bash üzerinden yapılan bir edit kontrol edilmez.
  • Bir bulgu, bildirilen satırlar dosyadan gittiğinde kapanır. Başka bir dosyaya taşınan bir satır bulguyu açık tutar.
  • deny modunun kaçış yolu yoktur. Bir bulgu düzeltilemediğinde kişi gate'i /sql-concat-watch mode note ile kapatır.
  • Gate komut metnini okur. git commit'i gizleyen bir script ya da alias üzerinden atılan commit durdurulmaz.
  • Bir git commit -a, bir -am ve -- 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.
  • Index, komut çalışmadan önce okunur. Okuma ile çalışma arasında dosyaları değişen bir commit, okuma anındaki index'e göre ölçülür.

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

sql-concat-watch

A Claude Code Mod that tells the model when an edit builds SQL by joining strings instead of passing query parameters. The note comes with the Edit's result and names each line, so the model rewrites the query in the same turn. By default nothing is stopped; in deny mode a commit, a push and a merge stop while a file still joins SQL.

What it does

  1. The mod hooks the Edit and Write tools. After a successful call on a source file (.ts, .tsx, .js, .jsx, .mjs, .cjs, .py, .php, .go, .rb, .java, .kt, .cs, .rs), it reads the lines the edit added: those of new_string that old_string does not have, or every line of a Write.

  2. A line counts when it holds SQL and joins a value into it:

    SQL Joins
    SELECT … FROM, INSERT INTO, UPDATE … SET, DELETE FROM, WHERE, VALUES, ORDER BY, GROUP BY, JOIN in upper case "…" + x and x + "…" (JS, Java, Go, C#, Kotlin)
    select * or select a, b from, insert into t (, delete from t where, update t set a = in any case Python f-strings, % and .format(
    PHP "… $var" and "…" . $var, Kotlin "… $var"
    C# $"… {x}", Ruby "… #{x}"
    fmt.Sprintf(, String.format(, format!(
    a template literal with ${…}, also over several lines

    A template literal whose tag passes the values as parameters is left alone: sql`…`, Prisma.sql`…`, prisma.$queryRaw`…`, $executeRaw`…`. $queryRawUnsafe is not such a tag. Comment lines and English prose ("select a file from the list: " + name) do not count.

  3. The model reads this note after the Edit's result:

    sql-concat-watch: this edit builds SQL from strings: src/db.ts:14 · src/db.ts:22. Pass values as query parameters (?, $1, :name) instead of joining them into the SQL text.
    

    The line number comes from the file after the edit; a Write is numbered from its own content. At most 8 places are named, the rest counted. When the file cannot be read, the path stands without a line and the error is logged once. The path is written against the directory the session started in when the file is inside it. That directory is read once at the session's start, because a Bash cd moves the session's own directory.

  4. The same moment writes one line to the transcript, so you see what the model was told. The line holds the places alone, without the instruction:

    sql-concat-watch: SQL built from strings: src/db.ts:14 · src/db.ts:22
    

    The note and the line are separate channels: the model never reads the line, and you never read the note.

  5. While the sidebar is open, those places go there instead, one line each, 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.

  6. A finding stays open until the file no longer holds those lines. After a later Edit or Write the mod reads each open file again, and a file whose lines are all gone closes. A file that is no longer there closes too, because it holds no line any more; a file that is there and cannot be read keeps its finding, because an unread file proves nothing:

    sql-concat-watch: the SQL built from strings is gone from src/db.ts: src/db.ts:14 · src/db.ts:22
    

    With the sidebar closed the same text is one transcript line. The model reads nothing of this: it rewrote the query itself.

  7. 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:

    sql-concat-watch: 2 place(s) still build SQL from strings: src/db.ts:14 · src/db.ts:22. Pass the values as query parameters (?, $1, :name), or take the lines out.
    

    One note per turn, not one per prompt. Without this the finding would be said once, at the edit, and then stand in the pane while the model forgot it. You read nothing new: the pane already carries the same finding.

  8. In deny mode the mod also stops git commit, git push and git merge while a file still builds SQL from strings. Before it stops one it reads each open file again, so a file the model fixed opens the gate itself. A git commit answers 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 open files, with one line to you naming how many still stand. A push and a merge hold no index to read, so every finding stands there. There is no bypass; only the person turns the gate off with /sql-concat-watch mode note. note mode is the default and stops nothing.

In the live check the model put db.query(`SELECT * FROM users WHERE id = ${id}`) into a file with one Edit, read the note naming src/users.ts:3, and named the parameterized form in its answer.

Command

/sql-concat-watch                 on or off, the mode, and the files still joining SQL
/sql-concat-watch on | off        on by default
/sql-concat-watch mode note       note only; the default
/sql-concat-watch mode deny       a commit, a push and a merge also stop while a file joins SQL

Install

claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install sql-concat-watch@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

  1. 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=sql-concat-watch}, turn.complete, prompt.submit, tool.call{tool=Bash}, tool.call{tool=Edit}, tool.call{tool=Write}
❯ ./register.ts calls: $.command.register, $.fs.exists (via isGone), $.fs.read (via fileText), $.process.run (via stagedPaths), $.session.cwd, $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get, $.store.set (via runCommand, setMode), $.ui.log (via fileText, gate, toPerson)

Reach L2, it runs git to read the index.

1. Reads:    the text of each Edit and Write call; the Bash command text; the edited file after an Edit that joins SQL, for the line numbers, and each open file again while a finding stands
2. Runs:     git rev-parse --show-toplevel and git diff --cached --name-only, at a commit in deny mode, to read which files the commit holds
3. Sends:    a note to the model after an edit that joins SQL, 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 edited text is only matched by regular expressions and printed as file:line, never run

Limits

  • The check is line by line with regular expressions, not a parser. A query built in a variable over several statements (q = "SELECT …"; q += id) is not seen.
  • A value joined into a table or column name, where a parameter cannot stand, is reported too. The note asks for a reason there, it stops nothing.
  • A query builder call (knex.raw, DB::raw, whereRaw) with a joined string is seen only when the string itself holds SQL keywords.
  • An edit through Bash is not checked.
  • A finding closes when the reported lines are gone from the file. A line moved to another file keeps it open.
  • The deny mode has no bypass. When a finding cannot be fixed, the person turns the gate off with /sql-concat-watch mode note.
  • The gate reads the command text. A commit through a script or an alias that hides git commit is not stopped.
  • A git commit -a, a -am and 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.
  • The index is read before the command runs. A commit whose files change between the read and the run is measured against what the index held at the read.

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