Context Engineering untuk AI Coding Agent di Monorepo

Foto oleh mooncow via Wikimedia Commons (CC BY-SA 3.0)
Karena baris yang disentuh sebuah task tersebar di package yang nyaris tidak berbagi apa pun selain satu repository, jadi cara termurah menemukannya adalah membuka file sampai ada yang kelihatan berhubungan. Semua yang sudah dibaca lalu tinggal di window dan dikirim ulang bersama setiap message berikutnya, sehingga file salah yang dibuka di menit pertama masih kalian bayar satu jam kemudian. Dokumentasi Claude Code menyebut context window sebagai sumber daya terpenting untuk dikelola justru karena itu.
Tidak. Claude Code memuat CLAUDE.md dari working directory dan semua directory di atasnya saat launch, tetapi file di subdirectory dimuat on demand, yaitu ketika Claude membaca file di directory itu. Itulah yang membuat file per package murah, karena konvensi package lain tidak pernah masuk ke session yang tidak menyentuhnya. Memecah file panjang menjadi import tidak memberi efek yang sama, karena import diekspansi saat launch.
Ada dua cara. Taruh SKILL.md di dalam package, misalnya di packages/api/.claude/skills/, supaya ia tersedia begitu Claude membaca file di sana. Atau simpan di root repository dan tambahkan field paths berisi glob pattern di frontmatter, yang membatasi aktivasi otomatis hanya pada file yang cocok. Cara kedua lebih baik ketika filenya tersebar, seperti setiap folder migrations di dalam tree.
Ya, karena subagent berjalan di context window terisolasi miliknya sendiri dan hanya final text response-nya yang kembali ke percakapan utama, plus metadata trailer kecil berisi hitungan token dan durasi. Walkthrough interaktif di dokumentasi menunjukkan research subagent membaca 6.100 token file dan mengembalikan hasil 420 token, dengan angka yang representatif dan bukan hasil pengukuran. Penghematannya hilang kalau kalian meminta ia mengembalikan kutipan filenya, bukan kesimpulan.
Jalankan command context setelah session mengerjakan sesuatu yang nyata, bukan saat startup: ia mencetak rincian langsung per kategori dan mendaftar file CLAUDE.md serta auto memory mana yang dimuat. Untuk log berjalan, tambahkan hook InstructionsLoaded. Input-nya membawa file_path, memory_type, dan load_reason seperti session_start, nested_traversal, atau path_glob_match, dan matcher-nya dicocokkan ke load_reason sehingga kalian bisa mencatat hanya lazy load.

Foto oleh mooncow via Wikimedia Commons (CC BY-SA 3.0)
Ringkasan Utama
Context engineering di monorepo intinya dua keputusan: instruction file mana yang dimuat bersama directory mana, dan search mana yang dijalankan di luar context window kalian sendiri. Claude Code memuat CLAUDE.md root saat launch dan CLAUDE.md sebuah package hanya ketika ia membaca package itu, dan file read milik subagent tidak pernah masuk ke window utama.
Perubahannya sekitar empat ratus baris dan tersebar di enam package: satu shared type, dua call site yang mengimpornya, satu migration, satu fixture, dan satu generated client yang seharusnya tidak disentuh siapa pun. Saya meminta semuanya dalam satu session, mulai dari root repository, lalu sepuluh menit pertama habis untuk menonton window terisi file yang tidak berhubungan dengan satu pun dari itu.
Itulah bentuk masalah context versi monorepo, dan ia bukan masalah yang sama dengan session yang terlalu panjang. Tulisan ini membahas mekanisme yang menanganinya — instruction file yang di-scope per directory, skill yang aktif lewat glob, search yang didelegasikan, structural lookup, dan daftar singkat hal yang sebaiknya tidak pernah masuk window — plus bagian yang paling sering salah ditebak pembaca untuk masing-masing: kapan ia sebenarnya dimuat. Perilaku pemuatannya diambil dari dokumentasi Claude Code sendiri. Urutannya adalah yang saya pilih setelah sempat melakukannya dengan buruk.
Monorepo tidak membuat context management lebih sulit secara umum. Ia mematahkan satu asumsi yang spesifik. Di luar monorepo, file yang disentuh sebuah task biasanya berdekatan, jadi membaca sekitar perubahan itu murah dan sebagian besar relevan. Di monorepo, empat ratus baris yang penting tersebar di package yang hanya berbagi satu repository dan nyaris tidak berbagi apa pun lagi, dan cara termurah menemukannya — buka file sampai ada yang kelihatan berhubungan — sekaligus cara tercepat mengisi window dengan hal yang ternyata tidak berhubungan.
Biayanya bukan pada membaca. Biayanya karena semua yang sudah dibaca itu tinggal. Context window menyimpan seluruh percakapan, termasuk setiap message, setiap file yang dibaca agent, dan setiap output command, lalu semuanya dikirim ulang bersama message berikutnya. Jadi file salah yang dibuka di menit ketiga masih kalian bayar di menit keempat puluh, dan di titik itu kalian juga membayar hukuman kedua yang disebut dokumentasi tanpa basa-basi: performa menurun seiring window terisi. Instruksi awal terlupakan, kesalahan bertambah, dan perbaikannya datang setelah penurunan itu, bukan sebelumnya.
Sebut dulu empat isi window sebelum mencoba mengecilkan salah satunya. Instruction yang selalu dimuat datang sebelum prompt pertama kalian: system prompt, memory file, nama tool, serta nama dan description setiap skill yang terdeteksi. Conversation history adalah semua yang sudah dibicarakan. Tool result adalah file read, output search, dan output command. Current task adalah prompt kalian. Dua dari empat itu layak diaudit dan dua tidak, dan garis pemisahnya adalah apakah kalian membayarnya sekali atau di setiap request.
| Penghuni | Kapan ia masuk | Dikirim ulang setiap turn | Tuas yang kalian punya |
|---|---|---|---|
| Instruction yang selalu dimuat | Sebelum prompt pertama | Ya, dan di-inject ulang setelah compaction | Scope per directory atau per glob |
| Conversation history | Seiring session berjalan | Ya, seluruhnya, sampai compaction | Clear di antara task yang tidak berkaitan |
| Tool result | Setiap read, search dan command | Ya, sampai session berakhir | Delegasikan pembacaannya ke subagent |
| Current task | Saat kalian menekan enter | Tidak, ini yang paling murah | Sebut nama filenya, bukan modulnya |
Dokumentasi menyediakan walkthrough interaktif tentang satu session yang terisi penuh, dan sekali mencobanya sudah berguna, dengan catatan yang mereka sebut sendiri: angka tokennya representatif, bukan hasil pengukuran. Bentuknya yang penting. Isi startup kecil dan permanen. Tool result besar dan permanen. Conversation history satu-satunya kategori yang bisa keluar, dan ia keluar dengan cara diringkas, sebuah pertukaran lossy yang syaratnya bukan kalian yang tentukan. Mengaudit bagian yang selalu dimuat itu pekerjaan sekali jadi. Menjaga tool result tetap di luar adalah keputusan yang kalian ambil beberapa kali dalam satu jam.
Pecah instruction per directory, dan aturan pemuatannya yang akan melakukan scoping untuk kalian. Claude Code memuat CLAUDE.md dari working directory dan semua directory di atasnya saat launch, lalu memuat file di subdirectory secara on demand ketika ia membaca file di sana. Jadi file root memuat hal yang berlaku di mana saja — konvensi commit, directory mana yang generated, cara menjalankan script sebuah package — sementara file tiap package hanya memuat stack-nya sendiri. Targetkan di bawah dua ratus baris per file; dokumentasi mencatat file yang lebih panjang memakan lebih banyak context sekaligus menurunkan kepatuhan, yang berarti buruk di dua sisi.
# The layout. Three packages, three sets of conventions, one root file.
monorepo/
CLAUDE.md # loaded at launch, from any starting directory
packages/
api/CLAUDE.md # loaded when Claude first reads a file in api/
web/CLAUDE.md # never loaded during an api-only session
shared/CLAUDE.md
# Root CLAUDE.md — only what is true in every package.
Run package scripts from the package directory, not the monorepo root.
Prefix commit subjects with the package name, for example: api: add rate limiting.
Never edit files under packages/*/generated/. Run npm run codegen in the package.
# packages/api/CLAUDE.md — nothing in here is true of the frontend.
Copy .env.example to .env before running anything. Tests fail without it.
Write database queries with the Knex query builder, never raw SQL in handlers.
Never edit a migration after it has merged. Add a new migration instead.Dua detail menentukan apakah ini benar-benar menghemat. Pertama, dari mana kalian memulai session. Dari root repository kalian dapat file root plus file setiap subdirectory begitu session menyentuhnya; dari packages/api kalian dapat package itu dan semua leluhurnya, dan konvensi frontend tidak pernah dimuat sama sekali. Kedua, jebakannya: import tidak menolong. File yang diimpor diekspansi dan dimuat saat launch bersama file yang mereferensikannya, maksimal empat hop, jadi memecah CLAUDE.md yang panjang menjadi import hanya merapikan, bukan menghemat.
// .claude/settings.local.json — for packages you never work in.
// Patterns are globs matched against ABSOLUTE paths, so a relative-looking
// pattern needs the leading two-star segment or it matches nothing at all.
{
"claudeMdExcludes": [
"**/packages/web/**",
"**/packages/legacy-*/**"
]
}
// This is a static list, not a per-task switch. To focus on one package
// today and a different one tomorrow, do not edit this file — start the
// session inside that package instead, which scopes the instruction
// files, the project settings and the in-scope skills in one move.
// And the thing that looks like scoping but is not: an @path import.
// CLAUDE.md
// @docs/api-conventions.md -> expanded and loaded AT LAUNCH
// @docs/testing-guide.md -> the same, four hops deep at most
// Splitting a 600-line CLAUDE.md into imports organises the file. It
// does not remove one token from what you pay for on every request.Kalau sebuah task hidup di satu package, mulai session di directory package itu, bukan di root repository. Itu satu-satunya keputusan scoping yang gratis untuk diambil, dan sekali diambil ia berlaku untuk instruction file, project settings, dan skill mana yang masuk scope, semuanya sekaligus. Simpan claudeMdExcludes untuk package yang memang tidak pernah kalian sentuh — ia daftar statis, bukan switch per task.
Konvensi tempatnya di instruction file. Prosedur tempatnya di skill, karena body sebuah skill baru dimuat saat dipakai, dan sebelum itu ia hanya membayar satu nama dan satu description. Di monorepo ada dua cara men-scope-nya. Taruh di dalam package, di mana SKILL.md di packages/api/.claude/skills/ menjadi tersedia begitu Claude membaca file di package itu. Atau simpan di root repository dan beri frontmatter-nya field paths berisi glob pattern, yang membatasi aktivasi otomatis hanya pada file yang cocok. Cara kedua paling pas untuk prosedur yang filenya tersebar — setiap folder migrations di dalam tree, di mana pun folder itu berada.
---
# .claude/skills/migration-review/SKILL.md at the REPOSITORY ROOT, but
# scoped by pattern rather than by placement. Use this shape when the
# files a procedure applies to are scattered across packages.
name: migration-review
description: Review a database migration before it merges. Use when writing or editing files under any migrations directory.
paths:
- "packages/*/migrations/**"
- "services/*/db/migrations/**"
---
## Checks, in order
1. Is the change additive? A merged migration is never edited.
2. Does the down migration actually reverse it, including the index?
3. Does any package still read the dropped column? Ask the language server.
# --- The other shape: put the skill IN the package ---
# packages/api/.claude/skills/api-testing/SKILL.md
#
# It becomes available when Claude reads a file in packages/api/. If the
# repository root also has an api-testing skill, BOTH stay available and
# the nested one appears as /packages/api:api-testing. Typing the plain
# /api-testing runs the root one, and Claude Code appends the qualified
# variants with an instruction to also invoke the one whose directory
# holds the files it is working on.Kegagalan di sini soal discoverability, bukan biaya. Claude memilih skill dengan membaca nama dan description setiap skill yang terdeteksi, dan dari root repository himpunan itu tumbuh seiring session menyentuh directory; dokumentasi memperingatkan jumlahnya bisa menumpuk sampai ratusan. Description dipendekkan ketika jumlahnya banyak, dan gabungan description dan when_to_use dibatasi 1.536 karakter di listing, jadi skill yang kata pembedanya ada di kalimat kedua bisa kehilangan kata itu tepat ketika ia bersaing dengan paling banyak saudara. Mulai dengan kata yang kemungkinan muncul di permintaan pengguna, dan sebut nama directory-nya di description.
Scoping menentukan apa yang datang bersama file. Ia tidak mengurangi pembacaannya sendiri, yang di monorepo justru separuh tagihan yang lebih besar, dan mekanisme untuk separuh itu adalah subagent.

Ini langkah dengan leverage terbesar di monorepo dan yang paling jarang saya lihat dipakai. Subagent mulai dengan context window baru yang terisolasi: ia tidak melihat conversation history kalian, skill yang sudah kalian invoke, maupun file yang sudah dibaca. Ia dapat system prompt sendiri, delegation message yang ditulis session utama, hierarki CLAUDE.md, dan isi penuh skill mana pun yang di-preload oleh definisinya. Setelah itu ia bisa membaca sebanyak apa pun file yang dibutuhkan pertanyaannya, dan hanya final text response-nya yang kembali ke kalian, plus metadata trailer kecil berisi hitungan token dan durasi.
---
# .claude/agents/monorepo-scout.md
# A read-only searcher. The tools allowlist is the point: it cannot edit,
# so a research errand can never quietly become a change you did not
# review, and it has no reason to open a file it will not report on.
name: monorepo-scout
description: Find every package that depends on a symbol, type or endpoint, and report the call sites with the assumption each one makes. Use before any cross-package change.
tools: Read, Glob, Grep, Bash
model: sonnet
---
You answer ONE question about a monorepo and then stop.
Return, in this order and nothing else:
1. The answer, in at most three sentences.
2. The paths that support it, one per line, with a line number.
3. Anything you could not determine, named as an open question.
Do not paste file contents. The session that called you can open any path
you name; it cannot un-read an excerpt you sent it.Walkthrough di dokumentasi memberi angka representatif untuk pertukaran ini: research subagent di contoh itu membaca 6.100 token file dan mengembalikan hasil 420 token. Jangan pakai angka itu sebagai benchmark — pakai rasionya sebagai kriteria pemilihan. Pertanyaan dengan search panjang dan jawaban pendek adalah kandidat ideal, dan package mana yang mengimpor type ini serta mana di antaranya yang menganggap nilainya tidak pernah null persis berbentuk seperti itu. Pertanyaan yang jawabannya sendiri panjang bukan kandidat, dan mendelegasikannya hanya memindahkan token satu hop.
# The delegation, from the main session. Name the agent, bound the
# question, and say what shape the answer takes. An unbounded
# "investigate the auth flow" is how a subagent returns a transcript.
Use the monorepo-scout subagent: which packages import OrderStatus from
packages/shared, and which of them assume the value is non-null? Answer
with the verdict and the paths. Do not include file contents.
# What crosses back into your window is the subagent's final text
# response plus a small metadata trailer with token counts and duration.
# Its own file reads stay in its window. A fresh subagent also does not
# see your conversation history, the skills you already invoked, or the
# files already read — so the question has to be self-contained.
# Wrong, and it took me two sessions to notice:
# "Use a subagent to read every call site of OrderStatus and show me
# what it found, so I can check it."
# That pays for the reading twice and lands the transcript in your
# window anyway. Ask for a conclusion and the paths behind it.Saya salah dengan cara paling jelas lebih dulu: saya mendelegasikan pembacaannya tapi meminta buktinya. Subagent yang mengembalikan empat puluh kutipan file supaya session utama bisa memeriksa hasilnya tidak memindahkan apa pun — kalian membayar pembacaan dua kali dan transkripnya tetap mendarat di window kalian. Minta kesimpulan plus path yang mendukungnya, tulis itu di delegation prompt, dan taruh instruksi yang sama di definisi agent supaya kalian tidak perlu mengingatnya lagi.
Di tempat language server tersedia, structural lookup menggantikan search-nya, bukan memangkasnya. Text search menjawab file mana yang memuat sebuah string. Pertanyaan monorepo hampir selalu di mana sebuah symbol didefinisikan dan siapa yang memanggilnya, dan dua jawaban itu berpisah di setiap re-export, alias, dan type bernama sama di package lain. Plugin code intelligence menyambungkan agent ke language server sehingga ia bisa melompat ke definisi, mendaftar reference, dan memunculkan type error setelah edit alih-alih menyisir tree: satu jawaban presisi, dibanding grep yang menyerahkan empat puluh hit tanpa peringkat apa pun.
Ini tidak gratis, dan biayanya perlu disebut terang-terangan. Plugin-nya butuh binary language server bahasa itu di mesin setiap developer, dan install dari marketplace resmi butuh akses jaringan ke GitHub, tempat marketplace itu di-host; di jaringan yang dibatasi, kalian menambahkan marketplace-nya dari Git host internal atau path lokal. Sebagai gantinya, inilah satu-satunya teknik di daftar ini yang mengurangi pembacaan tanpa menuntut kalian memutuskan apa pun saat itu juga.
Install code intelligence per bahasa, bukan per repository: jalankan plugin install untuk typescript-lsp dari marketplace resmi di dalam session, atau tambahkan ke project setting enabledPlugins supaya semua orang yang bekerja di repository itu mendapatkannya tanpa perlu install sendiri. Marketplace resmi menerbitkan plugin untuk TypeScript, Python, Go, Rust, dan bahasa umum lainnya.
Content search menghormati .gitignore secara default, jadi node_modules, dist, dan build sudah otomatis di luar hasil tanpa konfigurasi apa pun. Kasus yang mahal adalah yang di-commit orang, plus satu kebiasaan.
Blokir kasus yang sudah di-commit dengan Read deny rule, jangan mengandalkan penilaian saat itu juga. Deny rule mencakup file tool bawaan dan command file Bash yang dikenali — cat, head, grep, dan find — ketika path yang diblokir dilewatkan sebagai argumen, dan Claude Code juga berusaha sebaik mungkin menjaga path itu keluar dari hasil Grep dan Glob.
// .claude/settings.json at the repository root, committed.
// .gitignore already keeps node_modules, dist and build out of search
// results. These rules are for the paths somebody CHECKED IN.
{
"permissions": {
"deny": [
"Read(./**/generated/**)",
"Read(./**/*.generated.*)",
"Read(./vendor/**)",
"Read(./**/pnpm-lock.yaml)"
]
}
}
// Relative patterns anchor at the session's working directory, not at
// the repository root. If you start sessions inside packages, write them
// as double-slash absolute paths instead:
// "Read(//home/me/monorepo/vendor/**)"
// A worktree session loads project settings from the worktree root,
// which is the checked-out copy of THIS file — so these rules belong in
// the repository root's settings.json, not only in a package's.Deny rule adalah kontrol anggaran, bukan batas penegakan. Path yang diblokir tetap muncul di output search Bash seperti grep -r atau find, dan aturan itu tidak mencakup subprocess yang membuka file sendiri. Ia menghilangkan pembacaan tak sengaja dan godaannya; ia tidak menjamin sebuah file tidak akan pernah terlihat. Kalau sesuatu memang tidak boleh dibaca, tempatnya bukan di dalam repository.

Berhenti menebak separuh mana yang mahal. Jalankan command context setelah session mengerjakan sesuatu yang nyata, bukan saat startup: ia mencetak rincian langsung per kategori beserta saran optimisasi, termasuk file CLAUDE.md dan auto memory mana yang benar-benar dimuat. Startup adalah bagian yang sudah kalian tahu. Yang kalian butuh adalah keadaan setelah dua puluh tool call, ketika session sudah berkeliaran.
// .claude/settings.json — log every instruction file as it loads.
// The matcher runs against load_reason, so this one fires ONLY for the
// lazy loads: the nested CLAUDE.md files and the path-scoped rules a
// monorepo session picks up as it wanders across packages.
{
"hooks": {
"InstructionsLoaded": [
{
"matcher": "path_glob_match|nested_traversal",
"hooks": [
{
"type": "command",
"command": "jq -r '[.load_reason, .memory_type, .file_path] | @tsv' >> /tmp/loaded.tsv"
}
]
}
]
}
}
# After a real session, ask which packages it actually reached.
cut -f3 /tmp/loaded.tsv | sort | uniq -c | sort -rn
# 9 /home/me/monorepo/packages/web/CLAUDE.md <- an api-only task
# 4 /home/me/monorepo/.claude/rules/testing.md
# 1 /home/me/monorepo/packages/shared/CLAUDE.md
# Nine loads of another team's conventions is a claudeMdExcludes entry,
# or a session that should have started in packages/api. The hook cannot
# block or modify a load and its output is discarded — it exists for
# observability, which is exactly what an audit needs.Untuk separuh yang selalu dimuat, ada alat yang lebih tajam. Hook InstructionsLoaded menyala ketika sebuah instruction file dimuat, sekali di awal session untuk file yang eager dan kemudian lagi untuk yang lazy, dan input-nya membawa file_path, memory_type, serta load_reason berupa session_start, nested_traversal, path_glob_match, include, atau compact. Karena matcher-nya dicocokkan ke load_reason, kalian bisa mencatat hanya lazy load dan membaca ulang package mana yang benar-benar disentuh sebuah session. Pasangkan dengan logs exporter OpenTelemetry dan flag detail tool yang merekam nama skill secara verbatim, maka event skill_activated juga akan memberi tahu skill mana yang tidak pernah menyala — dan skill yang tidak pernah menyala itu menumpang sewa di listing setiap session.
Aturan yang sekarang saya pegang: monorepo tidak butuh context window yang lebih kecil, ia butuh setiap mekanisme dimuat di momen yang tepat. Instruction tempatnya di sebelah kode yang ia jelaskan. Prosedur tempatnya di belakang sebuah glob. Search tempatnya di window lain. File generated tempatnya di belakang deny rule. Audit separuh yang selalu dimuat satu kali, catat temuannya, lalu belanjakan perhatian yang kalian hemat untuk pembacaan, karena di situlah anggarannya benar-benar pergi.
Sumber dan bacaan lanjutan