כתיבה

הסוכן עומד בריפו אחד וכותב לתוך ריפו אחר: איך עצרתי את Claude Code מלעשות commit ו-push עיוורים

סוכני קוד כמו Claude Code מצוינים בתוך תיקייה אחת. הם קוראים פרויקט שלם, מאתרים באגים ומשחררים פיצ'רים. אבל ברגע שמשימה חוצה גבול בין מאגרים (repositories), משהו בסיסי נשבר, ואף אחד לא מתריע על זה.

במאמר הזה אתאר את הבעיה, את מה שלא עבד, ואת RepoGuards: סט קטן של הוקים (hooks) בקוד פתוח שרצים בתוך הסשן, ברגע שהסוכן קורא לכלי.


הבעיה: כל סשן רואה git status אחד

אצלי אפליקציה מתפרסת על כמה מאגרים: קוד האפליקציה, מאגר נתונים לחנויות (טקסטים, מחירים, צילומי מסך), ואתר. משימה שגרתית כמו "הכן את הגרסה הבאה" נוגעת בשלושתם.

הסוכן פותח סשן בתיקיית האפליקציה ומתחיל לעבוד. בשלב מסוים הוא צריך לעדכן את נתוני החנות, אז הוא מושיט יד החוצה עם git -C ../app-hub ....

מה שלא מובן מאליו: הסשן רואה git status אחד בלבד, זה של התיקייה שבה הוא עומד. מה שפתוח במאגר השני לא "חסר" לו. הוא פשוט בלתי נראה. ככה נאמר "סיימתי" מעל סטטוס נקי, בזמן שבמקום אחר נשאר בלגן.

מכאן נולדות שתי פעולות מסוכנות במיוחד:

  1. commit -a או add . במאגר זר. הדגלים האלה סוחפים כל מה שמלוכלך שם, כולל עבודה שהסוכן מעולם לא ראה.
  2. push למאגר זר. push מפרסם ענף שלם, לא רק את הקומיט האחרון. ב-7 בספטמבר 2026 זה שלח אצלי 20 קומיטים באחד ממאגרי האפליקציות, ו-18 מהם נכתבו לפני שהסשן בכלל התחיל.

בשורה התחתונה, בלי שום מנגנון הגנה, זה מה שקורה היום:

  • עבודה של מישהו אחר נכנסת לקומיט שלך. קבצים שמפתח אחר, או סשן מקביל, השאיר פתוחים נסחפים פנימה בשקט.
  • קומיטים ישנים מתפרסמים בלי שאיש החליט על כך. push אחד מוציא לעולם את כל מה שהצטבר בענף, ואין דרך להחזיר.
  • קבצים נדרסים במקום שאינו של הסוכן. הוא כותב לתוך נתיב שמאגר אחר מתחזק, בלי לדעת שמישהו שם אחראי עליו.
  • "סיימתי" נאמר מעל בלגן. הסטטוס שהסוכן רואה נקי, והעבודה הפתוחה במאגר השני פשוט נשכחת.

מה לא עבד: עוד שורה בקובץ ההוראות

הניסיון הראשון היה הכי מתבקש: לכתוב ב-CLAUDE.md "לעולם אל תעשה commit גורף מחוץ לתיקייה שלך". זה עובד, עד שזה לא עובד. בסשן ארוך עם קונטקסט עמוס, ההוראה היא עוד שורה אחת בין מאות.

הוראה היא בקשה. הוק הוא מנגנון. הוק של Claude Code רץ לפני כל קריאה לכלי, מקבל את הפקודה המדויקת, ויכול לסרב לה או להוסיף לה קונטקסט. הוא לא צריך שהמודל יזכור כלום.


מה RepoGuards עושה

RepoGuards יושב בין הסוכן לבין הפקודות שהוא מריץ. לפני שפקודה יוצאת לדרך, RepoGuards בודק לאן היא מכוונת. אם היא מכוונת למאגר שהסוכן לא עומד בו, RepoGuards מגיב באחת משלוש דרכים, לפי מידת הסיכון.

1. עוצר את מה שאין ממנו דרך חזרה

יש שתי פעולות ש-RepoGuards לא מאפשר במאגר זר: פעולה גורפת (commit -a, add .) ו-push. הראשונה סוחפת עבודה שהסוכן לא ראה, והשנייה מפרסמת אותה לעולם.

אבל לעצור זה רק חצי מהעבודה. סירוב סתמי בנוסח "Access Denied" רק שולח את הסוכן לנסות שוב בדרך אחרת. לכן RepoGuards מסביר למה הפעולה נעצרה, ומה כדאי לבדוק קודם.

זו ההודעה, מילה במילה, כפי שהגארד מדפיס אותה היום. זה הפלט של ההוק האמיתי על שני ריפואים זמניים שקרויים כמו שלי, לא דוגמה שנכתבה למאמר. נתקלתי בה לראשונה ב-29 בספטמבר, בזמן שפרסמתי את הפרויקט הזה. סשן שעמד בריפו של RepoGuards עצמו התבקש לדחוף את הריפו של App Factory שלידו, ואני כבר אישרתי לו את הדחיפה, בצ'אט.

`git push` targets the 'app-factory' repo (…/app-factory), which this
session is not standing in — this session is in …/cross-repo-guards.
A push publishes a WHOLE BRANCH, not the commit you just made, and it
cannot be undone — including commits that were there before this session.
It would publish 3 commit(s):
  d0fe45b 0.4.4: every script answers --help, and the docs match the tools
  ac5456f ci: clone RepoGuards beside the checkout, so the skipped checks run
  87b75ce evals: a first suite for app-store-review-compliance, and a runner
Look first, from here:
  git -C …/app-factory log @{u}..HEAD --oneline    # everything this would publish
  python3 $REPO_GUARDS/tools/close.py              # every place, and its push radius
Then either move the session to …/app-factory and push there, or ask
the owner to push. Do not push a branch you have no picture of.

האישור שלי לא נחשב, וזה בכוונה: ההוק לא יכול להבחין בין אישור אמיתי לאישור שהמודל דמיין, והוא גם לא מנסה. באותו יום הסוכן העביר את הסשן לריפו הנכון, קרא את שישה הקומיטים שעמדו לצאת (שלושה מהם בני שבועיים), ודחף משם. מאז הסירוב עושה את הקריאה בעצמו: הוא מונה את מה שהדחיפה הייתה מפרסמת, כך שהסוכן, וגם אתה, רואים את הענף כולו לפני שמשהו יוצא מהמחשב.

2. מראה לסוכן את מה שהוא לא רואה

רוב הפעולות במאגר זר הן לגיטימיות. למשל, כתיבה לקובץ מסוים או קומיט של נתיבים מפורשים. RepoGuards לא חוסם אותן, אבל הוא גם לא נותן להן לרוץ בעיניים עצומות. הוא מצרף לפקודה את תמונת המצב של המאגר השני: על איזה ענף הוא עומד, מה פתוח בו, וכמה קומיטים ממתינים לדחיפה.

[foreign] `git reset` reaches into 'repo-guards' (…/repo-guards), and this
session stands in …/app-factory. What is dirty there is invisible from here —
this is it:
  [current branch there: main]
  (clean — nothing uncommitted there)
  (13 commits there are unpushed — a push publishes all of them)
Stage only what you wrote. `python3 $REPO_GUARDS/tools/close.py` shows every
place at once.

RepoGuards לא מחליט כאן ולא מזהיר. הוא נותן לסוכן את מה שהיה רואה אילו עמד שם בעצמו, והסוכן מחליט על סמך תמונה מלאה.

3. מכבד גבולות בעלות

מאגר יכול להכריז שנתיבים מסוימים בו שייכים לו, בקובץ .claude/owns.json. RepoGuards מסרב לכתיבה לנתיבים כאלה מבחוץ. הוא מזהה לא רק את כלי העריכה של הסוכן, אלא גם כתיבה דרך הטרמינל: הפניה (>), sed -i, cp, mv, tee ו-rm. זה לא תמיד היה כך: עד 10 בספטמבר הוא חסם Edit, ו-> file עבר דרכו בלי שום בעיה.

ומה עוד. בסך הכול RepoGuards מורכב משבעה hooks. שניים מהם חוסמים, והשאר מדווחים. למשל, בסוף כל תור הוא אומר איזו עבודה נשארה בלי קומיט במאגר אחר, כדי שהיא לא תישכח.


סגירה: מה הסשן הזה כתב, בכל מקום

הצד השני של אותה בעיה הוא לסיים נכון. close.py מדווח על כל מקום שהסשן נגע בו: מה שינית, מה היה שם קודם, כמה קומיטים לא נדחפו, וכמה מהם קודמים לסשן. עם --commit הוא מכין לקומיט רק נתיבים שכלי כתיבה של הסשן הזה נקב בשמם, כך שעבודה של סשן מקביל לא נסחפת פנימה לעולם.

הוא עושה קומיט, אבל לא דוחף ולא מפרסם. שתי הפעולות האלה בלתי הפיכות. במקומן הוא נותן את מה שאי אפשר לעשות מהזיכרון: את טווח ה-push המדויק של כל מקום.


מה הוא לא יודע לעשות

  • הוא נכשל פתוח (fail open). גארד שחוסם בטעות עוצר עבודה, וגארד שקורס עוצר הכול. לכן כל שינוי ב-honor_ownership רץ על כל הענפים שלו לפני שהוא נכנס לתוקף.
  • כתיבה מתוך סקריפט היא מחוץ להישג שלו. ההוק רואה את הפקודה, לא את מה שהתוכנית עושה בפנים. זה כתוב במפורש ולא מוסתר.
  • "לא הוכרז" ו"שכחו להכריז" נראים לו אותו דבר. לכן יש בדיקה נפרדת בסוף כל תור, שמחפשת סקיל מקומי שאף אחד לא הכריז עליו.

סיכום: מהנדסת פרומפטים להנדסת מערכות

אי אפשר לסמוך על משמעת עצמית של מודל שפה בסשן של שלוש שעות. אפשר לסמוך על קוד שרץ לפני כל פקודה, מסרב לשתי הפעולות שאין מהן דרך חזרה, ומחזיר לסוכן את התמונה שהייתה חסרה לו.

RepoGuards הוא בקוד פתוח (MIT): github.com/yybd/repo-guards. הוא עומד בפני עצמו, ולא צריך שום דבר אחר כדי לעבוד.

בחלק השני: למה הפסקתי לכתוב קובץ הוראות ענק, ומה בא במקומו. 41 סקילים שנטענים רק איפה שהם נחוצים, ב-App Factory.