How to read a GitHub repository's code without cloning, with gh api
Back to the blog
Articlegithubdev toolshow toagente de programaçãoverboo code

How to read a GitHub repository's code without cloning, with gh api

MafraOctober 22, 20266 min read

Since 2026-09-09, 24 production runs of this blog used gh api to query GitHub, and 16 of them were Verboo Code tutorials written by reading the verbeux-ai/code repository without a single git clone. Each tutorial needs five to ten files from a repository with 3,115 entries. Cloning everything to read ten files is waste, and in the environment we run in (a VPS with an agent sandbox) it is not always possible.

This guide shows the gh (GitHub CLI) commands we use to map, filter, read and pin the version of a repository through the API alone. All of them were run on 2026-10-22 with gh 2.100.0. The API limits come from GitHub's official documentation, read the same day.

4-step flowchart: map the repository with git/trees?recursive=1, filter the folder with --jq, read the raw file with the Accept raw header, pin the version with ?ref; if the tree comes back truncated, walk one subtree at a time without recursive
The four steps, from the repository map to the file at the right version, with the branch for repositories that are too large.

How do you list every file in a GitHub repository without cloning?

With the git/trees endpoint and the recursive parameter. One call returns the path and size of every file on the branch:

gh api "repos/verbeux-ai/code/git/trees/main?recursive=1" \
  --jq '{truncated, total: (.tree|length), files: ([.tree[]|select(.type=="blob")]|length)}'

Output on 2026-10-22:

{"files":2737,"total":3115,"truncated":false}

The 3,115 entries include folders (type: "tree") and files (type: "blob"). The field that matters is truncated: if it is true, the list is incomplete. GitHub's documentation sets the limit at 100,000 entries and 7 MB per recursive response. Beyond that, request the tree without recursive and walk one subfolder at a time, using the sha of each tree entry.

One detail from the docs: any value in recursive turns recursion on, including 0 and false. To turn it off, omit the parameter.

How do you see only the files in one folder?

Filter the same tree by path prefix with --jq. That way you also see each file's size before opening it:

gh api "repos/verbeux-ai/code/git/trees/main?recursive=1" \
  --jq '.tree[] | select(.type=="blob" and (.path|startswith("src/commands/add-dir/"))) | "\(.size)\t\(.path)"'
4825	src/commands/add-dir/add-dir.tsx
260	src/commands/add-dir/index.ts
3207	src/commands/add-dir/validation.ts

For a single folder, the contents endpoint also lists files, without recursion:

gh api repos/verbeux-ai/code/contents/src/commands/add-dir --jq '.[] | "\(.type)\t\(.name)"'

The difference is the ceiling: the docs say contents lists at most 1,000 files per directory and point to the trees API beyond that. To find a file by name anywhere in the repository, the recursive tree is the shortest path.

This is the step we used to write the /add-dir tutorial: the tree showed the command's three files and, filtering by other prefixes, the permission and settings files it touches.

How do you read a file's contents from the terminal?

Ask the contents endpoint for the file with the raw media type header. You get the file's text, with no JSON and no base64:

gh api repos/verbeux-ai/code/contents/src/commands/add-dir/index.ts \
  -H "Accept: application/vnd.github.raw"
import type { Command } from '../../commands.js'

const addDir = {
  type: 'local-jsx',
  name: 'add-dir',
  description: 'Add a new working directory',
  argumentHint: '<path>',
  load: () => import('./add-dir.js'),
} satisfies Command

To find a line in a large file, pipe the output to grep -n, which also prints the line number:

gh api repos/verbeux-ai/code/contents/src/commands/add-dir/add-dir.tsx \
  -H "Accept: application/vnd.github.raw" | grep -n "import"

Gotcha: --jq does not work with the raw format

--jq expects JSON. With the raw header the response is plain text, and gh fails with this message:

invalid character 'i' looking for beginning of value

If you want to filter without a pipe (useful when the environment blocks |, as some agent sandboxes do), drop the header and decode the base64 inside jq itself:

gh api repos/verbeux-ai/code/contents/src/commands/add-dir/add-dir.tsx \
  --jq '.content | @base64d | split("\n") | to_entries[] | select(.value|test("import")) | "\(.key+1): \(.value)"'

This only works for files up to 1 MB. Above that, the content field comes back empty, as the table below shows.

What is the file size limit for reading through the GitHub API?

Up to 1 MB everything works. From 1 to 100 MB, only the raw format returns the contents. Above 100 MB, the endpoint does not serve the file. The numbers come from the official docs for the contents and trees endpoints:

SituationWhat happensWhat to do
File up to 1 MBEvery format worksRaw, or --jq with @base64d
File from 1 to 100 MBcontent is empty and encoding is "none"Use Accept: application/vnd.github.raw
File above 100 MBEndpoint not supportedClone, or download the release
Directory with more than 1,000 filescontents cuts the listUse git/trees
Recursive tree above 100,000 entries or 7 MBtruncated: trueTree without recursive, one subfolder at a time

In verbeux-ai/code, the largest file is 265,586 bytes, so everything fits in the first row of the table.

How do you read a file at a specific version?

Add ?ref= with the tag, branch or commit hash. That is what guarantees the tutorial describes the version the reader installed, not whatever changed on main afterwards:

gh api repos/verbeux-ai/code/releases/latest --jq .tag_name
gh api "repos/verbeux-ai/code/contents/package.json?ref=v0.15.38" \
  -H "Accept: application/vnd.github.raw" | grep -m1 '"version"'
v0.15.38
  "version": "0.15.38",

The tree accepts the same: replace main with the tag in git/trees/v0.15.38?recursive=1. And to know when a file last changed, the commits endpoint filters by path:

gh api "repos/verbeux-ai/code/commits?path=src/commands/add-dir/add-dir.tsx&per_page=3" \
  --jq '.[] | "\(.sha[0:7]) \(.commit.author.date[0:10])"'

Why not use gh search code to find the file?

Because code search has indexing rules that the tree does not, and sometimes it returns nothing without an error. On the same day, a search for a term that exists in the code came back empty on verbeux-ai/code and returned results on openai/codex:

gh search code "addDir" --repo verbeux-ai/code --limit 3
gh search code "useState" --repo openai/codex --limit 2 --json path --jq '.[].path'

GitHub's documentation lists the restrictions: only the default branch is indexed, only files smaller than 384 KB, only repositories with activity in the last year, and a fork is only included if it has more stars than its parent. We don't know which one caught our repository. The practical point is that an empty search does not prove the term is absent. The recursive tree lists everything, with no index in between.

Can a coding agent do this on its own?

Yes, and that is how our tutorials get written. The agent runs the tree, picks files by path and size, reads each one raw and cites the file and version in the text. A typical tutorial means five to ten files read in full, some with hundreds of lines. That is where reading cost adds up: on Verboo Code tokens are unlimited, so reading the whole file to check one flag costs the same as reading only the snippet you thought was enough.

Enjoyed this article?
Share knowledge with your network.
// Read also

Related articles