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.

  • Engineering Workflow
  • $codebase-onboarding
  • 190 lines
  • 3 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- codebase-onboarding

Installs to ~/.agents/skills/codebase-onboarding/ for every project on this machine.

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

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.

~/.agents/skills/codebase-onboarding/SKILL.md

--- name: codebase-onboarding description: Maps an unfamiliar repository in under an hour - build and test commands, entry points, request and data flow, module boundaries, conventions and hot… license: MIT ---

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:

  1. How do I build, run and test it? (exact commands that worked)
  2. Where does execution start? (entry points per deployable)
  3. How does one real request or job flow through the layers?
  4. Where are the boundaries? (modules, packages, services, what may import what)
  5. 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.

StepReadYou learn
1AGENTS.md (and nested ones), README.md, CONTRIBUTING.mdStated commands, rules, boundaries
2Manifests: package.json scripts, pyproject.toml, go.mod, Cargo.toml, Makefile, justfileReal commands, dependencies, workspaces
3CI config (.github/workflows/*.yml, .gitlab-ci.yml)The commands that actually gate merges, versions of tools
4Deploy config (Dockerfile, compose, k8s, fly.toml, vercel.json)Deployables and what each one runs
5Entry points and routing tablesWhere execution begins
6One end-to-end traceHow the layers actually connect
7Git history and ownershipWhere 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

StackLook for
Node/TSmain, bin, exports in package.json; scripts.start; src/index.ts, server.ts
Next.js / Remix / SvelteKitapp/ or src/routes/ file-system routes; middleware.ts
Python[project.scripts] in pyproject.toml; __main__.py; manage.py, urls.py; app = FastAPI()
Gocmd/*/main.go; every package main
Rustsrc/main.rs, src/bin/*.rs, [[bin]] in Cargo.toml
Java/Kotlin@SpringBootApplication, public static void main
Workers and jobsqueue 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 EDIT and for generator config before reading it closely.
  • Monorepos have several entry points and several test commands; one root npm test may 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.yamlgenerated from this page

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.py145 lines, 5.9 KB
#!/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.

esc