hew

module
v0.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT

README

hew

An opinionated, agentic-first CLI for tracking work in GitHub Issues. Inspired by beads, backed entirely by GitHub — native sub-issues and dependencies, priority/type/area labels, ready-work detection, and a hew prime command that injects tracker conventions and live state into a coding agent's context at session start.

GitHub Issues stays the single source of truth: humans get the web UI, PRs auto-close issues via Fixes #n, and nothing needs syncing.

Install

Needs git — the target repo is read from the local checkout, and hew pr reads the current branch — plus the gh CLI for authentication. The install script also needs curl and tar.

curl -fsSL https://lumberbarons.github.io/hew/install.sh | bash

Installs to ~/.local/bin (override with INSTALL_DIR); never uses sudo. Linux and macOS (x86_64 and arm64) only — elsewhere, use go install below.

Or, with a Go toolchain — the minimum is the go directive in go.mod, which tracks the latest patched release:

go install github.com/lumberbarons/hew/cmd/hew@latest

Authentication comes from the gh CLI — run gh auth login once and hew reuses its stored credentials. The target repository is detected from the git remote (--repo owner/name overrides).

Quickstart

hew init          # bootstrap the label set in a repo; prints a CLAUDE.md snippet
hew hooks install <claude|codex|cursor|opencode> # agent session-start hook running `hew prime`
hew prime         # session-start context: conventions + ready work + live state
hew ready         # what should I work on? (priority-sorted, zero open blockers)
hew start 42      # claim it: assign @me + in-progress (refuses claimed work: exit 3,
                  #   or exit 5 when the claim is already yours)
# ...branch (feat/|fix/|chore/), commit, push...
hew pr            # draft PR for the claimed issue, body composed, "Fixes #42" enforced

Session-start agents

hew prime works with Claude Code, Codex, Cursor, and opencode. Choose the agent explicitly:

  • Claude Code: hew hooks install claude adds a SessionStart hook to the project's .claude/settings.json.
  • Codex: hew hooks install codex adds the equivalent hook to the project's .codex/hooks.json. Codex requires project hooks to be trusted; review and enable it with /hooks.
  • Cursor: hew hooks install cursor adds a sessionStart hook to the project's .cursor/hooks.json. Cursor's stdout contract differs from the others': the hook must print valid JSON and the primer travels in the additional_context field (snake_case — Cursor ignores Claude Code's camelCase additionalContext), so the installed command is hew prime --hook-format cursor, which JSON-encodes the primer. Don't copy a Claude or Codex hook entry verbatim: bare hew prime text fails Cursor's JSON parse and the primer never lands.
  • opencode: hew hooks install opencode writes an auto-discovered plugin to .opencode/plugins/hew-prime.js; it injects the primer into system context, so it is present before the first turn without appearing in chat.

Both refuse to write through a symlink. A checkout is untrusted input, so hew never follows a link standing where it expects its own directory or settings file — including one that stays inside the checkout. If yours are symlinked into a dotfiles repo, edit the file the link resolves to directly.

hew ready prints one line per issue, so you can tell it worked:

#42 P1 bug  Retry loop hammers the API when offline

If any command exits 4, authenticate first with gh auth login.

Commands

hew prime                      # session-start context for agents
hew ready [--limit N]          # open, non-epic, triaged, zero open blockers; P0→P4
                                  # capped at 30 by default (0 for all); truncation warns
hew list [--label X] [--epic N] [--state open|closed|all]
            [--bodies]           # with --json: body on every line, dedup in one call;
                                   # untriaged issues are omitted — hew triage holds them
                                   # (--closed is an alias for --state closed)
hew show <n>                   # body, deps, parent, children, recent comments
hew search <terms>             # text search over vetted issues, open+closed, best-match
                                   # order — check for an existing issue before filing one
hew create --type bug|enhancement|task --title "..."
              [--where X] [--problem|--goal "..."] [--fix|--approach "..."]
              [--done-when "..."]...   # section flags compose the body template
              [--priority P0..P4] [--area X] [--blocked-by N] [--parent N]
              [--discovered-from N]
              [--body-file F | --edit] # long-form escape hatch / $EDITOR
hew start <n> [--priority P0..P4] [--force]
hew triage [--search <terms>]  # issues missing priority/type labels; --search runs
                                   # the same search over untriaged titles and bodies
                                   # (open+closed) — the untriaged half of dedup
hew set <n> [--priority ..] [--type ..] [--add-area X] [--remove-area X]
           [--parent N | --no-parent] [--title "..."]
           [--body-file F]        # replace the body (an empty file is refused)
           [--closed]             # set/block/unblock refuse a closed target — an
                                  # edit landing on one is almost always stale
                                  # state; this is the override
hew pr [--for N] [--title "..."]
          [--what "..."] [--why "..."] [--testing "..."]
                                  # draft PR for the claimed issue: body composed from
                                  # the issue (What/Why default to its Fix/Approach and
                                  # Problem/Goal), exactly one "Fixes #n", base is the
                                  # repo default. The title defaults to the issue title
                                  # under the type's commit prefix (bug → "fix: ...");
                                  # --title is passed through. Push the branch first.
          [--body-file F]         # long-form escape hatch (missing Fixes/Part of
                                  # trailers are appended, never duplicated)
          [--base BRANCH] [--ready]
hew close <n> --reason "..." [--completed | --duplicate-of M]
hew reopen <n> --reason "..."          # comment + reopen; releases a stale claim,
                                       # no-op on an open issue
hew block <n> --on <m> [--closed]      # native dependency, cycle-checked
hew unblock <n> --from <m> [--closed]
hew epic create --title "..." [--children N,N]
                   [--goal "..." --done-when "..." | --body-file F | --edit]
hew epic status [<n>]                    # rollup per open epic; with <n>, that epic's
                                         # children plus "next:" — the next workable
                                         # child (open, unblocked, in child order);
                                         # also reachable as: hew epic list
hew apply <plan.jsonl> [--dry-run] [--state F] [--throttle D]
                                 # batch-create a whole set of issues from a JSONL
                                 # plan — labels, bodies, parents, dependencies —
                                 # checkpointed and resumable (see "Plan files")
                                 # defaults: --state <plan>.state.json, --throttle 500ms
hew init
hew hooks install|remove <claude|codex|cursor|opencode>
                              # add/remove a session-start hook running `hew prime`
                              # in the selected agent's project configuration;
                              # preserves the rest of the file; needs a git repo
hew migrate beads [--file F] [--state F] [--throttle D]
                     [--dry-run] [--include-closed]
                                 # import a beads (bd) database: priorities, types,
                                 # deps, epics, in-progress state; resumable
                                 # defaults: --file .beads/issues.jsonl, --state
                                 # github-migration.json next to it, --throttle 500ms

Output is one compact line per issue, annotated with whatever keeps it from being plain ready work ([blocked by #120], [epic 2/6], [in progress @you]); list sorts ready work first, then claimed, blocked, and epics. Every command takes --json (stable flat schema; list commands emit NDJSON so output survives truncation and grep); every GitHub-touching command also takes --repo owner/name (hooks is local-only). Exit codes are meaningful: 3 means "already claimed by someone else, pick the next ready item", 5 means "already claimed by you, resume that work", 4 means "run gh auth login".

Checking for duplicates

Untriaged issues (missing a priority or type label) are held behind hew triage, so "does this already exist?" is answered in two calls, one per population:

  1. hew search <terms> — the default. Server-side, cheap, open and closed, vetted issues only, so "already fixed" answers the question as well as "already filed". Results are capped; it warns rather than paging, and when none of the matches it saw was triaged it says so — scoped to the fetched page when the results were capped — and names hew triage --search instead of pretending there is nothing to dedup against.
  2. hew triage --search <terms> — the same search over the untriaged queue, open and closed. Run it when the user asks, or in a scoped triage agent (see below); a coding agent that never runs it still dedups correctly against vetted work.
  3. hew show <n> — only to read a specific candidate the first two surfaced.

Then file with hew create ... --discovered-from <n>.

Both triage modes scan titles and bodies for suspicious Unicode and annotate the issue line with findings such as [contains zero-width characters], [contains bidi controls], [contains Unicode tags], or [contains confusable characters]. Confusables are non-ASCII characters that resemble ASCII letters or digits, including Greek/Cyrillic lookalikes and mathematical alphabets. Recognized Unicode 17 emoji sequences are exempt, including their joiners and flag tags; ordinary accents and punctuation are allowed. These are review hints: legitimate language or notation may trigger them, and issues are never hidden or rejected. Under --json, flagged issues carry a textFindings object with zeroWidth, bidiControl, unicodeTags, and confusable booleans; clean issues omit it.

Denying untriaged content in a harness

hew triage is the only command that emits untriaged titles and bodies (hew show <n> reading one issue by number is the deliberate exception), and the primer instructs agents never to run it unless the user explicitly asks. So the command name is the unit a harness deny list can key on: deny it, and an agent cannot reach unvetted content even by passing its own flags.

hew hooks install writes no permissions block — silently restricting an agent from a hook installer would be surprising. Add the deny entries yourself; in Claude Code's .claude/settings.json, for example:

{
  "permissions": {
    "deny": [
      "Bash(hew triage *)",
      "Bash(hew * triage *)"
    ]
  }
}

Two rules because hew's global flags parse in either position: the first covers hew triage and trailing flags, the second covers the same command with --json or --repo owner/name leading — hew --repo o/r triage would sail past the first rule's prefix. The * in the second rule matches anything between hew and triage, including more than one flag. Together they cover every valid arrangement of the command; a test in cmd/hew keeps the documented rules honest against the real flag surface.

The instruction and the deny entries are the two layers of the same boundary: the primer line holds for harnesses with no deny mechanism, and the deny entries enforce it where one exists. The natural split is two agent shapes — a coding agent denies hew triage and dedups with hew search, the right scope for an agent that can only act on vetted work; a scoped triage agent (like the auto-triage workflow below) is allowed triage plus show and does the untriaged half of dedup with hew triage --search.

Plan files

hew apply turns a multi-issue workflow — decomposing a spec into phase epics and tasks, filing a batch of review findings — into: write a plan, dry-run it, apply it. One JSON object per line:

{"id":"epic1","title":"Voltgo support","type":"epic","priority":"P1","goal":"..."}
{"id":"scaffold","title":"Scaffold the driver","type":"task","parent":"epic1","done-when":["driver builds"]}
{"title":"Wire the collector","type":"task","parent":"epic1","blocked-by":["scaffold",42],"areas":["ble"]}

type is bug|enhancement|task, or epic for a parent issue (no type label, Epic: title prefix). priority defaults to P2. parent and blocked-by take either a local id — a string, resolved to the created issue's number, so entries can reference each other before numbers exist — or an existing issue number. discovered-from adds the same origin link the create flag does. Bodies come from the same section fields the create flags use — where, problem or goal, fix or approach, done-when (a list, one checklist item each) — composed into the body template; body carries raw long-form text instead (mutually exclusive with the section fields).

Creation and dependency wiring are both checkpointed to the --state file as they happen, so a failed run resumes without creating duplicates or re-attempting edges that already landed; unknown fields, dangling references, and dependency cycles between entries are all rejected before anything is written.

The state file is trusted local scratch, not an input you can hand around: it records the repository and a digest of the plan it was written for, and every issue it maps is checked for a marker apply embedded in the body it created. A state file from another repository or another plan, one written by an older hew, or one pointing at an issue this plan did not create is refused before any write — including under --dry-run, so the plan-only pass reaches the same verdict as the real one. Editing the plan between runs invalidates the checkpoint by design; if you need to change it mid-flight, start a fresh run with a new --state path and expect the entries already created to be created again. The same rules apply to migrate beads and its snapshot.

Auto-triage in CI

examples/auto-triage.yml is a copy-pasteable GitHub Actions workflow that runs Claude with this CLI on every newly filed issue, so drive-by reports get deduped and labelled without a human sweep and ready stays truthful. To enable it:

hew init                        # once: create the convention labels
mkdir -p .github/workflows
curl -fsSL https://raw.githubusercontent.com/lumberbarons/hew/main/examples/auto-triage.yml \
  -o .github/workflows/auto-triage.yml
gh secret set ANTHROPIC_API_KEY    # paste a key from console.anthropic.com

The agent reads the new issue, searches open and closed issues for a duplicate, and either closes it as one or applies a type and priority label plus a short rationale comment. What it deliberately cannot do is the interesting part:

  • permissions: issues: write is the only grant — no code, no PRs, no other repos.
  • The tool allowlist is a handful of hew subcommands — including hew triage, since this is the scoped triage agent that runs the untriaged half of dedup — plus gh issue comment and gh label list. No create, no start, no shell beyond that.
  • It assigns P2–P4 only; anything it thinks is P0/P1 it labels P2 and flags for a human to upgrade. Reports too vague to classify are left untriaged (hew triage still lists them) rather than mislabelled.
  • Bot-filed issues and issues from anyone with write access are skipped, so hew create output and the workflow's own writes can't feed it back.

Issue bodies are untrusted input; the permission scope and the allowlist are the mitigation, not the prompt's instructions.

The same assumption shapes the read path: ready, prime, list and search all omit untriaged issues — prime runs as a SessionStart hook, and list and search are calls an agent can reach on its own initiative, flags included. GitHub drops labels from non-collaborators, which makes a priority and type label evidence that a maintainer saw the issue, and evidence a stranger cannot forge. Untriaged content is emitted by exactly one list-shaped command, hew triage, and the primer tells agents never to run it unless the user asks; the command boundary is what a deny list enforces (see above). The primer still reports how much is waiting.

Design

See DESIGN.md for the conventions the tool enforces, the read-path normalization rules, and the API strategy.

The token-lean claim is measured, not asserted: hew ready and hew list cost 17–25 tokens per open issue against 77–84 for the leanest equivalent GraphQL query and 113–141 for gh issue list --json, which cannot answer readiness at all. DESIGN.md has the full table, including the one command where the tool currently loses (list --json, filed as #62); evals/ is the harness that produced it.

Directories

Path Synopsis
cmd
hew command
Command hew is an agentic-first CLI for GitHub Issues.
Command hew is an agentic-first CLI for GitHub Issues.
internal
beads
Package beads parses a beads (bd) issues.jsonl snapshot — the canonical git-synced export, one JSON object per line.
Package beads parses a beads (bd) issues.jsonl snapshot — the canonical git-synced export, one JSON object per line.
cli
Package cli implements the commands against the gh.Client interface, so cmd/hew is pure wiring and everything with behavior is testable against a fake.
Package cli implements the commands against the gh.Client interface, so cmd/hew is pure wiring and everything with behavior is testable against a fake.
conventions
Package conventions holds the tool's opinions: the label set, the issue body template, and the static primer text.
Package conventions holds the tool's opinions: the label set, the issue body template, and the static primer text.
editor
Package editor opens $EDITOR on a seeded temp file and returns the edited text.
Package editor opens $EDITOR on a seeded temp file and returns the edited text.
gh
Package gh is the thin GitHub API layer: one interface the commands are written against, and a go-gh-backed implementation that reuses gh's stored credentials.
Package gh is the thin GitHub API layer: one interface the commands are written against, and a go-gh-backed implementation that reuses gh's stored credentials.
git
Package git reads the local branch state the pr command needs.
Package git reads the local branch state the pr command needs.
model
Package model holds the domain types and the pure read-path logic: label normalization, ready-work detection, and dependency-cycle detection.
Package model holds the domain types and the pure read-path logic: label normalization, ready-work detection, and dependency-cycle detection.
plan
Package plan parses the JSONL plan files consumed by `hew apply`: one entry per line, each describing an issue to create.
Package plan parses the JSONL plan files consumed by `hew apply`: one entry per line, each describing an issue to create.
render
Package render turns domain types into the two output forms: compact fixed-column text and flat JSON.
Package render turns domain types into the two output forms: compact fixed-column text and flat JSON.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL