Codebase Onboarding skill for Codex
Answer how this repo works with file paths, not guesses. Codex reads an unfamiliar repository in a fixed order (AGENTS.md, manifests, CI, deploy config, entry points) instead of opening files at random, then traces one real request through every layer with file and line hops. A bundled read-only script inventories languages, manifests, tests and git churn, and the result is a one-page orientation note where every command was actually run.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- codebase-onboarding Installs to ~/.agents/skills/codebase-onboarding/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install codebase-onboarding The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project codebase-onboarding Installs to .agents/skills/codebase-onboarding/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/codebase-onboarding.zipunzip -o codebase-onboarding.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) codebase-onboarding PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $codebase-onboarding, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Maps an unfamiliar repository in under an hour - build and test commands, entry points, request and data flow, module boundaries, conventions and hot spots - by reading manifests, AGENTS.md, CI config and git history before opening random files, then writes a short verified orientation note. Use when someone says "explain this codebase", "where do I start", "how does a request get from the route to the database", when joining a project, inheriting a repo, or before a first change in code nobody on hand understands.
Use it when
- You are new to a repository and need to make a change safely
- Someone asks where a feature lives or how a request flows
- Documentation is missing or out of date and you need a verified map
- You are preparing onboarding material for new hires
Use something else when
- You need the history behind one specific line or decision: use code-archaeology
- You want to write the repo's agent instructions file: use agents-md-authoring
- You need a formal architecture diagram for stakeholders: use c4-architecture-diagrams
How to check the result
Every command in the orientation note ran successfully in the session, and the traced request path was confirmed by a test, a log line or a local call.
The mistake it prevents
Trusting README commands over CI config, then reporting a build or test command that has not worked for months.
Onboarding is done when you can answer five questions with file paths, and each answer was checked by running or tracing something rather than guessed from names:
- How do I build, run and test it? (exact commands that worked)
- Where does execution start? (entry points per deployable)
- How does one real request or job flow through the layers?
- Where are the boundaries? (modules, packages, services, what may import what)
- What conventions must a change follow? (style, errors, tests, naming, review rules)
Order of reading
Read in this order; each step narrows the next. Do not open source files at random.
| Step | Read | You learn |
|---|---|---|
| 1 | AGENTS.md (and nested ones), README.md, CONTRIBUTING.md | Stated commands, rules, boundaries |
| 2 | Manifests: package.json scripts, pyproject.toml, go.mod, Cargo.toml, Makefile, justfile | Real commands, dependencies, workspaces |
| 3 | CI config (.github/workflows/*.yml, .gitlab-ci.yml) | The commands that actually gate merges, versions of tools |
| 4 | Deploy config (Dockerfile, compose, k8s, fly.toml, vercel.json) | Deployables and what each one runs |
| 5 | Entry points and routing tables | Where execution begins |
| 6 | One end-to-end trace | How the layers actually connect |
| 7 | Git history and ownership | Where change happens, who knows what |
CI beats README when they disagree: README commands rot, CI commands run every day.
Get a fast inventory first:
python3 scripts/repo_map.py . # languages, manifests, entry points, tests, CI, churn
git ls-files | wc -l # tracked files only, skips build output
git ls-files | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -25 # busiest dirs
repo_map.py is read-only and uses only the standard library; it reports git churn when
the folder is a git checkout.
Workflow
- [ ] 1. Read AGENTS.md, README, CONTRIBUTING; note every command they claim
- [ ] 2. Run repo_map.py; list deployables (services, CLIs, libraries, jobs)
- [ ] 3. Reproduce build + test locally; record what actually worked
- [ ] 4. Find entry points for each deployable
- [ ] 5. Trace one real flow end to end, writing file:line hops
- [ ] 6. Draw the dependency direction between top-level modules
- [ ] 7. Extract conventions from code, not just docs
- [ ] 8. Identify hot spots and owners from git history
- [ ] 9. Write the orientation note (template below)
Step 3: prove the commands
Run the cheapest checks first: install, typecheck or compile, unit tests. In a Codex sandbox, dependency installs usually need network; if network is off, say so and check for a populated lockfile cache or vendored deps instead of failing silently. Record the exact command, its duration, and any env var it needed. Never run migrations, seeders or deploy scripts against shared environments while exploring; read them instead.
Step 4: find entry points
| Stack | Look for |
|---|---|
| Node/TS | main, bin, exports in package.json; scripts.start; src/index.ts, server.ts |
| Next.js / Remix / SvelteKit | app/ or src/routes/ file-system routes; middleware.ts |
| Python | [project.scripts] in pyproject.toml; __main__.py; manage.py, urls.py; app = FastAPI() |
| Go | cmd/*/main.go; every package main |
| Rust | src/main.rs, src/bin/*.rs, [[bin]] in Cargo.toml |
| Java/Kotlin | @SpringBootApplication, public static void main |
| Workers and jobs | queue consumers, cron definitions, celery, sidekiq, bullmq registrations |
rg -n "package main$" --type go -l
rg -n "if __name__ == .__main__.:" --type py -l
rg -n "@(app|router)\.(get|post|put|delete|patch)\(" --type py | head -40
rg -n "\.(get|post|put|delete|patch)\(['\"]/" --type ts | head -40
Step 5: trace one real flow
Pick the most business-critical path (checkout, login, ingest), not the simplest. Follow it from the outside in and write each hop down:
POST /orders
-> src/api/routes/orders.ts:41 validates body with OrderSchema
-> src/services/orders.ts:88 createOrder(): opens transaction
-> src/repos/orderRepo.ts:22 INSERT orders, order_items
-> src/events/publish.ts:15 emits order.created (outbox table)
-> workers/fulfilment/index.ts:30 consumes order.created
Confirm the trace dynamically when it is cheap: run the relevant test, add a temporary log line or breakpoint, or call the endpoint locally. Remove temporary instrumentation before finishing. A trace you only read is a hypothesis.
Step 6: boundaries and dependency direction
rg -n "^(import|from) " --type py | awk -F: '{print $1, $3}' | head -50
rg -n "from ['\"](\.\./)+" --type ts | head -30 # deep relative imports cross boundaries
Write the allowed direction as a one-line rule (api -> services -> repos -> db; domain
imports nothing internal). Note every violation you see; they are where changes surprise
people. If the repo already enforces this (eslint-plugin-boundaries, import-linter,
internal/ packages in Go, Nx tags), cite the config.
Step 7: conventions from code
Docs describe intentions; code shows practice. Sample three recent, reviewed files in the area you will touch and note:
- Error style (exceptions vs result types, custom error classes, where they are mapped to HTTP)
- Logging (library, structured fields, request IDs)
- Test style (framework, fixture/factory helpers, naming, where tests live)
- Config and secrets (env loading, typed settings object, feature flags)
- Formatting and lint configs (
.editorconfig,ruff.toml,eslint.config.js,biome.json)
Step 8: hot spots and owners
git log --since=6.months --name-only --format= | sort | uniq -c | sort -rn | head -20
git shortlog -sn --since=1.year -- src/billing/ # who knows this area
git log --oneline -15 -- src/billing/ # recent intent, in commit messages
High churn plus high size marks the files where bugs and merge conflicts concentrate. For
why a specific odd line exists, hand off to code-archaeology techniques (git log -S).
Gotchas
- Generated code (protobuf stubs, ORM clients, OpenAPI clients) looks like source. Check
for headers like
DO NOT EDITand for generator config before reading it closely. - Monorepos have several entry points and several test commands; one root
npm testmay run only a subset. Read the workspace config (pnpm-workspace.yaml,go.work, Nx/Turbo). - Dependency injection and decorators hide call edges. Search for the registration site
(
container.register,@Injectable,providers:), not only the call site. - Feature flags and env-based branches mean the code path you read may be off in production. Note every flag on the traced path.
- Dead directories survive for years. Before learning a module deeply, check that it is imported and has recent commits.
Output: orientation note
Keep it under one screen per section; link paths instead of pasting code. Offer to save
it as docs/onboarding.md or to fold the commands into AGENTS.md, but ask first.
# <repo> orientation (verified <date>, commit <short sha>)
## Commands (all run successfully)
- Install: `pnpm install` (needs network)
- Test: `pnpm test` (~40 s); single file: `pnpm vitest run path`
- Run locally: `pnpm dev` + `docker compose up db`
## Deployables
| Name | Entry point | Runs as |
|---|---|---|
## One request, end to end
<hop list with file:line>
## Module map and dependency rule
<5-10 lines; the allowed direction; known violations>
## Conventions to follow
<errors, logging, tests, naming - each with an example path>
## Hot spots and owners
<top churn files, who to ask>
## Open questions / not verified
<anything read but not run>
Verification
- Every command in the note was executed in this session; anything not executed is listed under “not verified”.
- The traced flow was confirmed by a test, a log line or a local request.
- A teammate (or a fresh agent) could make a one-line change and run its test using only the note.
What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Codebase Onboarding"
short_description: "Answer how this repo works with file paths, not guesses"
default_prompt: "$codebase-onboarding I just inherited this repo - explain how it is structured and how to run it" scripts/repo_map.py
#!/usr/bin/env python3
"""Print a first-hour map of a repository: languages, manifests, entry points,
test locations, CI and agent instructions. Standard library only, read-only.
Usage: python3 scripts/repo_map.py [repo_dir] [--depth N]
"""
from __future__ import annotations
import argparse
import os
import subprocess
import sys
from collections import Counter
from pathlib import Path
SKIP_DIRS = {
".git", "node_modules", "vendor", "dist", "build", "out", "target", ".venv", "venv",
"__pycache__", ".next", ".nuxt", ".turbo", ".cache", "coverage", ".idea", ".vscode",
"Pods", ".gradle", ".terraform", "bin", "obj", ".mypy_cache", ".pytest_cache",
}
EXT_LANG = {
".py": "Python", ".ts": "TypeScript", ".tsx": "TypeScript", ".js": "JavaScript",
".jsx": "JavaScript", ".mjs": "JavaScript", ".cjs": "JavaScript", ".go": "Go",
".rs": "Rust", ".java": "Java", ".kt": "Kotlin", ".swift": "Swift", ".rb": "Ruby",
".php": "PHP", ".cs": "C#", ".c": "C", ".h": "C/C++ header", ".cpp": "C++",
".cc": "C++", ".scala": "Scala", ".ex": "Elixir", ".exs": "Elixir", ".dart": "Dart",
".vue": "Vue", ".svelte": "Svelte", ".sql": "SQL", ".tf": "Terraform", ".sh": "Shell",
".lua": "Lua", ".zig": "Zig",
}
MANIFESTS = {
"package.json": "Node package", "pnpm-workspace.yaml": "pnpm workspace",
"turbo.json": "Turborepo", "nx.json": "Nx", "pyproject.toml": "Python project",
"setup.py": "Python (legacy setup.py)", "requirements.txt": "Python requirements",
"go.mod": "Go module", "go.work": "Go workspace", "Cargo.toml": "Rust crate/workspace",
"pom.xml": "Maven", "build.gradle": "Gradle", "build.gradle.kts": "Gradle (Kotlin)",
"Gemfile": "Ruby/Bundler", "composer.json": "PHP/Composer", "mix.exs": "Elixir/Mix",
"Package.swift": "SwiftPM", "pubspec.yaml": "Dart/Flutter", "Makefile": "Make targets",
"Taskfile.yml": "Task runner", "justfile": "just recipes", "Dockerfile": "Container build",
"docker-compose.yml": "Compose stack", "compose.yaml": "Compose stack",
}
AGENT_DOCS = ["AGENTS.md", "AGENTS.override.md", "README.md", "CONTRIBUTING.md",
"ARCHITECTURE.md", "docs/architecture.md", ".github/CODEOWNERS", "CODEOWNERS"]
ENTRY_NAMES = {
"main.py", "__main__.py", "app.py", "manage.py", "wsgi.py", "asgi.py", "main.go",
"main.rs", "lib.rs", "index.ts", "index.js", "server.ts", "server.js", "main.ts",
"app.ts", "Program.cs", "Application.java", "main.dart", "App.tsx", "page.tsx",
"layout.tsx", "routes.rb", "urls.py", "cli.py",
}
TEST_HINTS = ("test", "tests", "spec", "__tests__", "e2e", "integration")
def walk(root: Path, max_depth: int):
for dirpath, dirnames, filenames in os.walk(root):
rel = Path(dirpath).relative_to(root)
depth = 0 if str(rel) == "." else len(rel.parts)
dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS and not d.startswith("."))
if depth >= max_depth:
dirnames[:] = []
yield rel, dirnames, filenames
def git(root: Path, *args: str) -> str:
try:
return subprocess.run(["git", "-C", str(root), *args], capture_output=True,
text=True, timeout=30, check=True).stdout
except (OSError, subprocess.SubprocessError):
return ""
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("repo", nargs="?", default=".")
ap.add_argument("--depth", type=int, default=6, help="max directory depth to scan")
args = ap.parse_args()
root = Path(args.repo).resolve()
if not root.is_dir():
print(f"error: {root} is not a directory", file=sys.stderr)
return 2
langs: Counter[str] = Counter()
manifests: list[str] = []
entries: list[str] = []
test_dirs: set[str] = set()
top_sizes: Counter[str] = Counter()
for rel, dirnames, filenames in walk(root, args.depth):
top = rel.parts[0] if rel.parts else "(root)"
for name in dirnames:
if name.lower() in TEST_HINTS:
test_dirs.add(str(rel / name))
for f in filenames:
p = rel / f
lang = EXT_LANG.get(Path(f).suffix.lower())
if lang:
langs[lang] += 1
top_sizes[top] += 1
if f in MANIFESTS and len(manifests) < 40:
manifests.append(f"{p} ({MANIFESTS[f]})")
if f in ENTRY_NAMES and len(entries) < 40:
entries.append(str(p))
print(f"# Repo map: {root.name}\n")
print("## Read first")
found = [d for d in AGENT_DOCS if (root / d).exists()]
print("\n".join(f"- {d}" for d in found) or "- (no AGENTS.md / README found)")
print("\n## Languages (source files)")
for lang, n in langs.most_common(8):
print(f"- {lang}: {n}")
print("\n## Where the code lives (source files per top-level dir)")
for d, n in top_sizes.most_common(12):
print(f"- {d}/: {n}")
print("\n## Manifests and build files")
print("\n".join(f"- {m}" for m in manifests) or "- none found")
print("\n## Likely entry points (by filename)")
print("\n".join(f"- {e}" for e in entries) or "- none matched; check manifest scripts/bin")
print("\n## Test locations")
print("\n".join(f"- {t}/" for t in sorted(test_dirs)[:20]) or "- no test dirs by name")
ci = [p for p in (".github/workflows", ".gitlab-ci.yml", ".circleci", "Jenkinsfile",
"azure-pipelines.yml", ".buildkite") if (root / p).exists()]
print("\n## CI")
print("\n".join(f"- {c}" for c in ci) or "- none found")
log = git(root, "log", "--since=6.months", "--name-only", "--format=")
if log:
churn = Counter(l for l in log.splitlines() if l.strip())
print("\n## Most-changed files, last 6 months (where work happens)")
for f, n in churn.most_common(10):
print(f"- {n:4d} {f}")
else:
print("\n## Git history: unavailable (not a git repo or git missing)")
return 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.