CLI usage

The file-concat CLI runs the same @fileconcat/core library that powers the web app. Same filtering semantics, same output format, no browser required.

Install

npm install -g @fileconcat/cli
# or
pnpm add -g @fileconcat/cli

Node 18 or newer. The published package is @fileconcat/cli; the bin command it installs is file-concat.

You can also invoke it without a global install:

npx @fileconcat/cli ./src

Usage

file-concat [path] [options]
file-concat concat [path] [options]

The bare command and the explicit concat subcommand share the same flag set. [path] defaults to the current directory.

Flags

FlagShortDefaultNotes
[path].Positional argument; root to process
--output <file>-ooutput.xml / output.mdDefault extension follows --style. Ignored when --stdout set.
--style <style>-sxmlxml, markdown, or md
--max-size <mb>-m32Per-file size cap in megabytes
--no-hiddenhidden files excludedPass to force-exclude after a config opt-in
--no-binarybinary files excludedPass to force-exclude after a config opt-in
--exclude <patterns...>-eRepeatable; one or more glob patterns
--config <file>-cPath to a JSON config file
--no-parsedocuments are extractedPass to skip text extraction entirely — PDF/Office, notebooks and transcripts alike
--expand-archivesarchives left packedUnpack zip, tar and gzip archives and include their contents
--line-numbersPrefix each line of file content with its line number
--no-gitignore.gitignore honoredPass to ignore the project's .gitignore files
--stdoutWrite the concatenated output to stdout. Mutually exclusive with --json.
--quiet-qSuppress progress logs on stderr. Errors still print.
--jsonEmit a single-line JSON summary on stdout when finished.

Defaults for --no-hidden and --no-binary reflect that hidden and binary files are already excluded out of the box. The flags are there to flip a per-project override back to the default.

Example

file-concat ./src \
  --exclude "**/*.test.ts" \
  --exclude "**/__snapshots__/**" \
  --style markdown \
  --output context.md

This walks ./src, drops every test file and snapshot directory, emits Markdown with fenced code blocks, and writes to context.md.

Config file

The CLI looks for a config file in the working directory at startup. Names searched in order:

  1. .fileconcatrc
  2. .fileconcatrc.json
  3. fileconcat.config.json

Pass --config <path> to point at a different file. The format is JSON. A minimal example:

{
  "version": 1,
  "maxFileSizeMB": 32,
  "exclude": ["**/*.test.ts", "**/__snapshots__/**"],
  "style": "markdown",
  "output": "context.md"
}

Command-line flags override config-file values for the same fields.

Documents (PDF, DOCX, XLSX, PPTX, ODT, ODS, ODP, RTF)

Documents are extracted to text by default and inlined alongside your code. Pass --no-parse to turn that off and let them fall through as binaries instead.

file-concat ./repo               # documents extracted (default)
file-concat ./repo --no-parse    # documents left out as binary

Which files qualify is decided by their leading bytes, not their name. A .docx someone renamed .zip is extracted, a PDF with no extension at all is read, and a genuine .zip is never mistaken for the Office container that shares its signature.

Parse failures (corrupt files, password-protected PDFs, scanned pages with no text layer) are logged to stderr and counted under skippedBreakdown.parseFailed in the JSON summary. They never fail the run.

Notebooks, transcripts and messages (IPYNB, SRT, VTT, EML)

These are already text, so they used to go in whole — which meant base64 PNGs of every plot in a notebook, MIME boundaries and base64 attachments in a saved message, and a cue index plus a timestamp range for every two seconds of a transcript. All three are now rendered instead:

  • .ipynb becomes markdown. Prose cells verbatim, code cells fenced in the notebook's own language, text output and tracebacks kept. Images and other binary output are dropped and counted, so the summary says how much was left behind.
  • .srt / .vtt become the transcript. Indices, timestamps and styling go; a line that a rolling caption repeats in the next cue is emitted once. Timestamps are dropped rather than thinned — a transcript is read for what was said.
  • .eml becomes the correspondence: From, To, Cc, Date, Subject, then the body, quoted-printable and MIME-encoded headers decoded, and an HTML-only message flattened to its words. Attachments are named in the text and counted, never inlined and never silently dropped.

Same rule as documents: the leading bytes decide, so a notebook saved as .json, a transcript saved as .txt, and a message saved with no extension at all are recognized. .msg — Outlook's own format — is not read yet. --no-parse turns this off along with document extraction and leaves the raw file in the bundle.

Archives

Archives are left packed by default. Walking a directory that happens to contain assets.zip is not a request to inline it, and doing so would bury the code you actually asked for. Pass --expand-archives when you do want the contents:

file-concat ./repo --expand-archives

Zip, tar, and gzip (including .tar.gz) are unpacked; each entry lands under a folder named after the archive and faces the same size, binary and extraction handling as a loose file. Nesting is one level deep — an archive inside an archive stays packed. .rar and .7z are recognized but cannot be opened yet, and are reported as skipped.

For AI coding agents

The CLI is designed to drop into Claude Code, Cursor agent mode, aider, or any custom orchestrator without surprises. Three contracts hold:

  1. Stdout is the artifact, stderr is the chatter. Progress lines and warnings always go to stderr. Stdout only carries the concatenated output (with --stdout) or the JSON summary (with --json). file-concat ./repo --stdout 2>/dev/null is a clean pipe; file-concat ./repo --json 2>>logs.txt preserves progress.
  2. --json provides a single-line machine-readable summary.
    {
      "files": 42,
      "parsed": 3,
      "skipped": 5,
      "skippedBreakdown": { "oversize": 1, "binary": 2, "readError": 0, "parseFailed": 2 },
      "totalBytes": 184320,
      "outputPath": "output.xml",
      "elapsedSeconds": 0.213,
      "style": "xml"
    }
    
    The summary appears on stdout, so harness code can pipe it directly into JSON.parse.
  3. Exit codes are stable. 0 on success (including partial-skip outcomes), 1 on any fatal error or flag conflict. Errors are written to stderr with an Error: prefix.

Recipes

Pipe a directory straight into an LLM CLI:

file-concat ./service --stdout --quiet | claude -p "explain this codebase"

Generate context plus a machine-readable summary for a wrapper script:

file-concat ./service -o ctx.xml --json | jq '.parsed'

Pull a shipped archive's contents in alongside the source:

file-concat ./service --expand-archives -o ctx.xml

See also

  • File filtering covers the glob syntax, which is shared with the web app's filter textareas.
  • Configuration describes the web app's parallel configuration surfaces.