Count lines of code in a GitHub repository without cloning it

Updated 2026-09-25 · FreeTools

In short: curl 'https://freetools.one/v1/loc?github=torvalds/linux' returns a per-language breakdown with files, lines, blanks, comments and lines of code. No key, no clone, no install.

Counting lines of code sounds trivial until you try to do it consistently. cloc and scc give you a number, but they want the repository on disk and they disagree about what counts. The harder problem is doing it for a repository you do not have — a dependency, a candidate you are evaluating, or a public repo behind a link.

The basic request

curl 'https://freetools.one/v1/loc?github=torvalds/linux'

The response is a flat array of language objects, with a final Total entry. Keeping the shape flat means you can pipe it straight into jq:

curl -s 'https://freetools.one/v1/loc?github=cli/cli' \
  | jq '.[] | select(.language=="Total") | .linesOfCode'

Understanding the numbers

FieldWhat it counts
filesFiles of that type that were counted
linesEvery physical line, including blanks and comments
blanksLines that are empty or whitespace only
commentsLine and block comment lines
linesOfCodelines − blanks − comments
percentShare of total code lines

Knowing the split matters. A repository with 40,000 lines but only 8,000 lines of code is mostly documentation or generated output, and the plain "lines" number hides that.

Excluding directories and files

Use ignored for anything you do not want counted. It accepts a comma-separated list of files and directories:

curl 'https://freetools.one/v1/loc?github=org/repo&ignored=vendor,dist,*.snap'

A small set of directories is skipped automatically because they are almost never source code: node_modules, vendor, dist, build, target, out, .next, coverage, venv and .venv. Generated bundles such as *.min.js and any file that looks binary are also excluded.

Counting a specific branch

curl 'https://freetools.one/v1/loc?github=org/repo&branch=develop'

Omit the parameter to use the repository's default branch, which is detected automatically.

Getting the file list and extensions

Add detail=1 when you need to go below the language level. The response then contains three keys: languages, extensions and files.

curl -s 'https://freetools.one/v1/loc?github=cli/cli&detail=1' | jq '.extensions[:5]'

extensions is the breakdown by file type, which is often more revealing than the language breakdown — a repository that is mostly .json or .yml is a very different project from its language mix suggests. files gives a row per file with its path, size and line counts, which is what you want to find the biggest file in a codebase.

GitLab works the same way

Nested groups are supported, so pass the full namespace path:

curl 'https://freetools.one/v1/loc?gitlab=gitlab-org/gitlab-runner'

From Python, for a CI job

import requests

stats = requests.get('https://freetools.one/v1/loc',
                     params={'github': 'cli/cli'}).json()
total = [s for s in stats if s['language'] == 'Total'][0]
print(f"{total['linesOfCode']} LOC in {total['files']} files")

Size limits

Repositories up to 300 MB compressed and 1.5 GB uncompressed with up to 200,000 files are supported. Anything larger is rejected with HTTP 413 instead of timing out, so your script gets a clear error rather than hanging.

Open the LOC counter Compare with star history

Reading the result well

The most common mistake is reading lines and calling it "lines of code". For a documentation-heavy repository those numbers can differ by an order of magnitude. A quick health check looks like this:

curl -s 'https://freetools.one/v1/loc?github=org/repo' | jq '
  (.[-1]) as $t |
  { files: $t.files,
    code_share: (($t.linesOfCode / $t.lines * 100) | floor) }'

A repository under 50% code share is usually carrying a lot of generated or documentation content, which is worth knowing before you draw conclusions from the total.

Finding the biggest files

With detail=1 the files array carries a row per file, so the ten largest files in a repository are one jq call:

curl -s 'https://freetools.one/v1/loc?github=org/repo&detail=1' | jq '
  .files | sort_by(-.linesOfCode)[:10]
  | .[] | "\(.linesOfCode)\t\(.path)"'

Comparing repositories

Because the response is plain JSON, comparing two projects is a loop:

for repo in torvalds/linux cli/cli BurntSushi/ripgrep; do
  n=$(curl -s "https://freetools.one/v1/loc?github=$repo" \
      | jq -r '.[] | select(.language=="Total") | .linesOfCode')
  printf "%-28s %s LOC\n" "$repo" "$n"
done

Results are cached for six hours, so a loop over twenty repositories finishes in about a second rather than twenty seconds.

Which languages are recognised

The counter understands around 35 languages by extension, plus files with no extension where a shebang identifies them — a runme script starting with #!/usr/bin/env python is counted as Python, not skipped.

Comment syntax is handled per language, including C-style block comments, SQL -- and /* */, Lua's --[[ ]], and HTML or Markdown <!-- -->. If a language is not recognised its files are simply not counted, which is why the sum of the per-language numbers always equals the Total row.

Why line counts differ between tools

You will eventually compare this number with cloc or tokei and see a difference. That is expected, and it comes down to three decisions:

None of these choices is universally right. What matters is that they are consistent, so numbers stay comparable across runs and across repositories.

Frequently asked questions

How do I count lines of code in a GitHub repository?

Send GET /v1/loc?github=user/repo. The response lists files, lines, blanks, comments and lines of code for every detected language, plus a Total row, without needing to clone the repository.

Can I count a single file's lines of code?

The API counts whole repositories because it reads the source archive. Request detail=1 and filter the files[] array by path to get the exact numbers for one file.

Which files are excluded automatically?

Common dependency and build directories (node_modules, vendor, dist, build, target, out, .next, coverage, venv, .venv), generated bundles like *.min.js, and any file containing binary data are skipped.

How large a repository can the LOC API handle?

Up to 300 MB compressed, 1.5 GB uncompressed and 200,000 files. Larger repositories are rejected with HTTP 413 rather than timing out.