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:
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.
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.
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.