hardwood-hq/hardwood/hardwood-cli
Claude Code and agent skill by hardwood-hq/hardwood
- community
Checking that the SKILL.md file is still in the repository.
Inspect, debug, and convert Apache Parquet files with the `hardwood` CLI. Use whenever a user is debugging code that reads or writes Parquet (Arrow / Spark / Flink / Java / Python) and the data file itself may be the cause — to check the schema, physical and logical types, repetition, or a row sample. Use it to diagnose query-optimizer behavior: why row-group pruning, min/max predicate pushdown, or page-index filtering isn''t skipping data. Use it to read per-column and per-row-group compressed sizes and codecs, page-level min/max statistics and the page index, dictionary-encoded values, the footer / PAR1 header, or to convert rows to CSV or JSON. Trigger on `.parquet` files, "is this column dictionary encoded", "why are all row groups scanned", "why isn''t pushdown working", "what types does this parquet have", "convert parquet to csv/json", and any question about row groups, column chunks, data pages, the page index, statistics, or encodings.
Install
npx vskill@latest install hardwood-hq/hardwood/hardwood-cliSKILL.md
Inspecting Parquet files with hardwood
hardwood is a command-line tool for looking _inside_ Apache Parquet files without writing any code. It is purpose-built for the moments when you are debugging Parquet read/write code and start to suspect the file itself — a type that does not match, statistics that are missing, an encoding that defeats your optimizer, a dictionary that is not what you expect.
This skill teaches you which subcommand answers which question, how to read the output, and how to chain a few commands into a diagnosis.
Before you start
- Confirm the binary is available. The CLI ships as a GraalVM native binary
with instant startup. Check for it before assuming a flag will work:
``shell command -v hardwood && hardwood --version ``
- Every command takes `-f`/`--file`. This is the only required flag for the
read-only commands. It accepts a local path or an s3:// URI (no extra config beyond the standard AWS environment variables AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, etc.):
``shell hardwood schema -f s3://my-bucket/path/to/data.parquet ``
- `-h`/`--help` on any command lists its real options — run it when a flag
below does not match what you see, e.g. hardwood inspect pages --help.
- `dive` needs a TTY; you usually do not.
hardwood diveis an interactive
terminal UI. It exits with an error when stdin/stdout is not a real terminal, which is the normal case for an agent. Use the headless subcommands below for your own investigation, and _suggest_ dive to the human user when they want to poke around a file interactively.
Command quick reference
| You want to know… | Run | ||
|---|---|---|---|
| The high-level shape of a file (row groups, rows, sizes, writer) | hardwood info -f FILE | ||
| The schema: fields, physical types, logical-type annotations | hardwood schema -f FILE | ||
| The same schema as Avro or Protobuf | hardwood schema -f FILE -F AVRO / -F PROTO | ||
| A few actual rows, formatted | hardwood print -n 20 -f FILE | ||
| The raw file layout: size, footer offset/length, PAR1 magic | hardwood footer -f FILE | ||
| Per-column size, ranked: share, compression, encoding + dictionary cardinality | hardwood inspect columns -f FILE | ||
| Level histograms for one column: nulls vs empty lists | hardwood inspect columns -f FILE --column PATH | ||
| Per-row-group column chunks: type, codec, sizes | hardwood inspect rowgroups -f FILE | ||
| Pages (data + dictionary) and their min/max stats | hardwood inspect pages -f FILE [-c COLUMN] | ||
| The dictionary entries of a column | hardwood inspect dictionary -f FILE -c COLUMN | ||
| Rows out as CSV or JSON | `hardwood convert -f FILE -F csv\ | json` | |
| Embedded metadata from other engines (Arrow, Spark, pandas) | hardwood info -f FILE, --kv-key KEY | ||
| Interactive exploration (hand off to the user) | hardwood dive -f FILE |
Key output signals to read for
A lot of the diagnostic value is in small labels the commands print. Learn to spot these:
- `info` → `Created By:` reveals the writer library and version
(parquet-cpp-arrow version 24.0.0, parquet-mr, spark, …). Writers differ in whether they emit statistics, page indexes, and which encodings they use — this line is often the first clue in a "pushdown doesn't work" mystery.
- `info` → `Key/Value Metadata (N):` lists file-level metadata other engines
embed alongside the data — ARROW:schema (Arrow), org.apache.spark.sql.parquet.row.metadata (Spark), pandas (pandas/PyArrow). This is the first thing to check when the same file reads differently in two engines: values are truncated by default, so use --kv-key <name> to print one entry in full.
- `inspect pages` → the `(stats: …)` suffix after the column name tells you
whether page-level statistics exist and where they come from:
(stats: ColumnIndex)— a Page Index (ColumnIndex + OffsetIndex sidecars)
is present. This is what engines need for page skipping.
(stats: inline)— only legacy statistics embedded in each data-page header.
No page index, so no page-level skipping.
(no page-level stats)— nothing at page level. Nothing can be skipped at
page granularity.
(stats: mixed)— different row groups disagree; some skipping only.- `inspect pages` → the `Min` / `Max` / `Nulls` columns show the actual
bounds per page. If the predicate value falls _inside_ every page's min/max, skipping nothing is correct behavior, not a bug.
- `inspect columns` → `Share` is the column's percentage of the file's
compressed bytes: the row at the top (Rank 1) is the dominant scan cost. `Compression` is compressed ÷ uncompressed for the named Codec; 100.0% means the codec achieved nothing. A # Pages count (vs —) confirms a page index exists for that column.
- `inspect columns` → `Unencoded` is what the values occupy with no
encoding, which predicts read-side memory — a dictionary-encoded column can be small on disk and large once materialised. The file records it for BYTE_ARRAY; for a fixed-width type it is the present-value count times the width. It is — only where the present-value count is unknown.
- `inspect columns` → `Encoding` names what the data pages use, read from
encoding_stats: DICT, PLAIN, DELTA, and so on. The ranked table shows the union across row groups; --column PATH breaks it out per row group, and dive's column chunk detail shows it for a single chunk.
- `inspect columns` → the percentage after `DICT` is the dictionary's
cardinality: entries ÷ values it could hold an entry for. This is the number to scan for. DICT <1% is a dictionary doing its job. `DICT 100%` means every value is distinct, so the column is stored twice — once verbatim in the dictionary page, once as a stream of distinct indices — and no codec undoes the second copy, because distinct indices are high-entropy by construction. A high-cardinality column stays dictionary-encoded silently: writers dictionary-encode by default and fall back only when the dictionary outgrows its size limit, so one that stays under the limit is never reconsidered. What to use instead is a question the metadata cannot answer — re-encode and compare on disk. DELTA_BYTE_ARRAY for sorted or prefix-sharing strings and DELTA_BINARY_PACKED for near-sequential integers are the usual answers. PLAIN+DICT is the one worth acting on — the writer started with a dictionary and abandoned it mid-chunk when it outgrew its size limit, leaving a dictionary page that earns nothing for the pages after the fallback.
- `inspect columns --column PATH` → `tags null` vs `tags empty` distinguishes
a record whose field was absent from one whose list was empty. Nothing else in the metadata separates them: Nulls and the page index null_counts lump both together.
- `inspect dictionary` → `no dictionary (column is not dictionary-encoded)`
versus a table of entries: tells you whether a column is dict-encoded and what its distinct values are.
- `footer` → magic bytes
PAR1at both ends = a plain Parquet file. An
encrypted footer (Parquet Modular Encryption) does not print a magic value: footer instead exits non-zero and writes Encrypted Parquet files are not supported (Parquet Modular Encryption). to stderr — that error, not a PARE string in the output, is the signal the file needs decryption keys.
Playbook: "the source Parquet file looks wrong"
The user is debugging read/write code and has narrowed it down to a suspect file. Establish ground truth in three commands:
What to compare against the code:
- Physical vs. logical type mismatches. A field the code treats as a
String may physically be BYTE_ARRAY with a STRING annotation, or just bare BYTE_ARRAY. schema prints both, e.g.: `` message schema { required int64 id; optional binary category (STRING); optional int32 ts (TIMESTAMP_MILLIS); } ``
- Repetition / nullability.
requiredvsoptionalchanges null handling. - `Created By` — if it names a library version known to emit odd encodings,
that is a lead.
To export a sample for a downstream tool, project just the columns you care about (nested fields use dot notation) and pick head or tail:
Row-limit semantics for -n/--rows (used by both print and convert): positive = first _N_ rows (head), negative = last _N_ rows (tail), ALL = every row, and 0 is rejected.
--skip <n> starts the read at row _n_ and --row-group <i> reads that row group only, both counted from 0, on print and convert. They cannot be combined with each other, nor with a negative -n (which counts from the end of the file). A positive -n stays within the selected row group. Negative or out-of-range positions and empty row groups are rejected. print --row-index numbers rows by their position in the file, including under a negative -n, so a row number seen in hardwood dive reproduces here.
Playbook: "why isn't predicate pushdown / file skipping working?"
A query is scanning far more data than expected. The engine skips at two granularities — row group (using each column chunk's footer Statistics) and page (using the Page Index). Walk down the file to see what skipping is even possible:
Interpretation checklist:
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/hardwood-cli/SKILL.md
Popularity
- Repository stars
- 357
- Repository forks
- 78
- 7-day trend score
- 29
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/hardwood-hq/hardwood/hardwood-cli)