neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기grep by meaning, across languages. TypeSafe Jev scores every line against a meaning; combine meanings with AND/OR/NOT. 意味で探す grep。日本語で英語を、英語で日本語を検索できる
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
sys1grep — System 1 for your files; uses TypeSafe AI's Jev Background and design notes (Japanese): Jevのキラーアプリ、「意味で探す grep」を作った on Zenn.README 전문 보기 ↗
Background and design notes (Japanese): Jevのキラーアプリ、「意味で探す grep」を作った on Zenn.
Renamed from semgrep (0.5.0). Two reasons. The name collided with the static-analysis tool Semgrep, a trademark. And the tool was never meant for Jev alone: it targets System 1 models in general, and I'd like it to work with any models TypeSafe AI may release after Jev under other names :). So from 0.5.0 this is sys1grep (System 1 + grep): npm package
@uehaj/sys1grep(was@uehaj/semgrep), commandssys1grep/git sys1grep, environmentSYS1GREP_*, config~/.config/sys1grep/.env. The oldSEMGREP_*names and~/.config/semgrep/.envstill work for one minor release, each use printing a deprecation line; 1.0.0 drops them. Details: CHANGELOG. The Zenn article's URL keeps its old slug.
A grep that finds lines by what they mean, not by regular expressions. Matching is done by Jev, the System One model from TypeSafe AI. Jev does not generate text. It answers typed questions with probabilities, so for every line sys1grep asks "does this line match the meaning network failure?", gets a probability back, and applies a threshold.
sys1grep -n -e "customer is angry or frustrated" tickets.txt
▶ Click the demo, or open sys1grep.js.org, for the full demo on the landing page.
fetch, nothing else.The meaning and the text do not have to share a language. Jev compares concepts, not words, so one query finds matching lines in every language the file contains.
An English meaning finds Japanese lines. None of the hits contain "angry" or "frustrated", and two of them are in Japanese:
$ sys1grep -n -e "customer is angry or frustrated" tests/corpus.txt
14:ユーザー山田さんからの問い合わせ: 返金してほしい、商品が壊れていた
16:ユーザー佐藤さんからの問い合わせ: 注文した覚えのない請求が来ています。至急確認してください
18:I want my money back. The item arrived broken and customer service ignored me.
21:Your product ruined my weekend. Never buying from you again.
23:This is the third time I'm writing. Nobody has replied to my previous emails.
A Japanese meaning finds English lines, with the same confidence as the Japanese ones:
$ sys1grep -n -p -e "返金の要求" tests/corpus.txt
14:ユーザー山田さんからの問い合わせ: 返金してほしい、商品が壊れていた [0.97]
18:I want my money back. The item arrived broken and customer service ignored me. [0.95]
It is not a Japanese/English feature. tests/multi.txt holds refund requests and thank-you notes in French,
Russian, German, Spanish, Chinese and Korean. One Japanese meaning finds all six refund requests; a Russian
meaning does the same:
$ sys1grep -n -p -e "顧客が返金を求めている" tests/multi.txt
1:Je veux être remboursé, le produit est arrivé cassé. [0.98]
3:Я требую вернуть деньги, товар не работает. [0.97]
5:Ich möchte mein Geld zurück, das Gerät ist defekt. [0.97]
7:Quiero un reembolso, el paquete llegó vacío. [0.97]
9:我要求退款,商品坏了。 [0.97]
11:환불해 주세요. 제품이 고장났어요. [0.97]
$ sys1grep -n -p -e "клиент требует возврат денег" tests/multi.txt
1:Je veux être remboursé, le produit est arrivé cassé. [0.94]
3:Я требую вернуть деньги, товар не работает. [0.97]
5:Ich möchte mein Geld zurück, das Gerät ist defekt. [0.92]
7:Quiero un reembolso, el paquete llegó vacío. [0.89]
9:我要求退款,商品坏了。 [0.92]
11:환불해 주세요. 제품이 고장났어요. [0.89]
This makes sys1grep useful for mixed-language logs and ticket dumps, and for teams whose members query in different languages. One caveat: TypeSafe documents English as the most accurate language, and in our tests Japanese meanings wobble a little more near the threshold. When a query is borderline, phrasing the meaning in English is the safer choice.
If all you want is "lines about X", embedding similarity gives you the same lines. sys1grep differs in what it judges: not how close a line is to a topic, but whether a proposition holds for that line. Jev reads the line and the question together (a cross-encoder shape), so who did what, negation, and "asked for" versus "already done" all change the answer. An embedding of the line is fixed before it ever sees your query, so it can only measure topical closeness.
The first six lines below are "about a refund", and the last two are the pair the AND example further down uses (the same contrast as tests/contrast.txt, translated to
English as tests/contrast.en.txt — see Search across languages above for
the case where the meaning and the text differ in language). Only two are a customer asking for one:
$ sys1grep -n -p -t 0 -e "customer is asking for a refund" tests/contrast.en.txt
1:I want a refund. The item was broken. [0.99]
2:The refund has been processed. Please check your account. [0.11]
3:Our refund policy is within 30 days of purchase. [0.09]
4:The manager denied the refund request yesterday [0.17]
5:I demand a full refund immediately [0.93]
6:Refunds are processed within 5 business days [0.07]
7:The support agent got angry and hung up the phone. [0.05]
8:The customer got angry and hung up the phone. [0.06]
Because each meaning yields an independent probability, logical AND and NOT are plain boolean operations, not a trick with set differences or "negative queries":
# about a refund, but NOT a customer asking for one → completed, policy, denied, timelines
$ sys1grep -n -e "about a refund" -v "the customer is asking for a refund" tests/contrast.en.txt
2:The refund has been processed. Please check your account.
3:Our refund policy is within 30 days of purchase.
4:The manager denied the refund request yesterday
6:Refunds are processed within 5 business days
# angry AND it is the customer, not the staff
$ sys1grep -n -e "someone is angry" -a "the customer, not the staff, is the one acting" tests/contrast.en.txt
5:I demand a full refund immediately
8:The customer got angry and hung up the phone.
Line 7, The support agent got angry and hung up the phone., scores 0.05 on the second meaning and is
excluded, even though it differs from line 8 by one word (support agent vs. customer) — wording an
embedding would place right next to line 8's. (We could not reproduce a cosine-similarity number for
this pair: no embedding-model access was available in this environment, and no prior measurement script
exists in the repository or its history to rerun. The argument stands on the wording alone: an embedding
of line 7 has no way to see that "support agent" changes who the sentence is about.)
Two more practical consequences. The probabilities are calibrated, so one threshold (0.5) works across queries, where cosine scores need top-k or per-query tuning. And there is no index to build: sys1grep reads the files in front of you. The flip side is that every query pays for the whole corpus again, so for repeated queries over a large, fixed corpus a vector index is cheaper and faster.
-P '/pattern/flags' (first and last character /, JavaScript flags) starts a regex term, OR'd like
-e, and is matched locally with no request at all. -a/-v '/pattern/flags' AND / AND NOT a regex
onto the preceding term, the same shape deciding regex from meaning there. It prefilters its AND term:
only the lines it holds for ever ask that term's meanings, so a cheap regex in front of a meaning cuts
both the bill and the wait. A line is sent only if some term's regexes all hold for it (a term with no
regex holds for every line), so with -P '/re/' -a A -e B a line without re is still sent, asked B
only. A query of regex terms alone sends nothing, even with --unit=sentence-by-jev: the rules alone
then decide where wrapped lines join. -P '!/RE/' is not allowed; negate a regex with -v '/RE/'
instead. -e never reads its value as a regex: a /RE/flags-shaped value there is refused, with a
hint naming -P (e.g. -e '/catch/' is no longer a regex: use -P '/catch/'). -Q is unaffected: it
was already always a question, never a regex, so -e '/etc 以下のファイルを変更している' (no closing
/) still goes through as a meaning, unrefused. A meaning for -e/-a/-v that really starts and
ends with / can still be written with a leading space to dodge the regex reading (-e ' /cat/'
sends the text " /cat/", not a refusal).
$ sys1grep -P '/ERROR|FATAL/' app.log # no requests at all
$ sys1grep -P '/timeout/i' -a '顧客に影響が出ている' app.log # only lines with "timeout" go to Jev
A regex's named and numbered groups pass to the other meanings of the same AND term as
$<name>, $1-$99, $&, $$ — ECMAScript's replacement-pattern syntax
(String.prototype.replace's GetSubstitution), with one deviation: $<name> naming no group is
an error rather than an empty string (so is a reference to a negated regex's group). A $n naming
no group stays literal, as in ECMAScript, so $100 以上の請求 is unaffected.
$ sys1grep -P '/(?<date>\d{4}-\d\d-\d\d) (?<time>\d\d:\d\d)/' \
-a '$<time> が深夜(0時〜5時)であり、$<date> が週末である' app.log
# 2026-09-19 03:12 ... → asks "03:12 が深夜(0時〜5時)であり、2026-09-19 が週末である"
Prefer $<name> and single quotes: $<name> survives double quotes in sh/bash/zsh; $1, $time
and ${time} don't (the shell expands them itself). -p prints 1.00/0.00 for a regex term.
On a terminal every match of a regex is in grep's match color (bold red), for the terms that held; a negated regex
is never colored. -o prints each match on a line of its own, as grep -o; a line only meanings matched prints
whole, since a meaning has no matching part.
$ sys1grep -o -n -P '/[A-Z]+-\d+/' -a 'the ticket is still open' notes.txt # the ticket ids, one per line
Every line sent to Jev costs money and time, so the cheapest line is the one never sent. From the widest cut to the narrowest:
-r skips .git, node_modules, binary files, likely secrets, generated files (source maps,
minified JS/CSS, lock files) and what git ignores; git sys1grep searches
tracked files only. --include / --exclude (file-name globs) and --changed-within (30m, 7d, today,
this-week, a date) narrow them further. A meaning that restricts itself to a language or a time of change
narrows them by itself: see Scope from the meaning.-M/--max-columns characters of a line
(default 2000, 8000 with -z) are sent: it is still searched and judged, just not past that cutoff.--dedup judges one line per template: lines that
differ only in ids, numbers, times or paths share one answer. auto/always/never, off by default for
now: a hint says when --dedup=auto would pay.--cache keeps Jev's answers in ~/.config/sys1grep/cache, keyed by the request sent (model,
lines, questions) and the endpoint's host, so the same search again sends nothing and costs nothing. It is off
by default, since the cache holds the lines searched; SYS1GREP_CACHE=on (in the environment or
~/.config/sys1grep/.env, not settings.json) sets the default. --cache=only answers from the cache alone: a
request not there is exit 2 unless -q already found a match, named, and nothing is sent. --dry-run says how
many requests are cached and how many it would send. There is no expiry: the model is in the key, and Jev
drifts about ±0.05 between runs, so a cached answer is one of those runs. empties it.$ sys1grep --dry-run -r --include='*.log' --changed-within=today -P '/ERROR|FATAL/' -a 'a customer is affected' logs/
--verbose (or --dry-run) also shows where a setting that was not typed on the command line came from
(SYS1GREP_OPTS, an environment variable, ~/.config/sys1grep/settings.json or .env, or a preset's default), so a result that
surprises you can be traced back to its source. The key's value never appears, only which option or variable
supplied it:
$ SYS1GREP_OPTS='--level strict' sys1grep --verbose -e "the API key is read from a file" .
sys1grep: endpoint api.typesafe.ai/v1/systemone (default), model jev-latest (default)
sys1grep: key: key (~/.config/sys1grep/settings.json)
sys1grep: SYS1GREP_OPTS: --level strict
sys1grep: options: --level strict (SYS1GREP_OPTS) = -t 0.7 -T 0.3, --chunk 30, -j 8, scope on, -M 2000, --max-filesize 10M, --max-cost 1
sys1grep: file ./a.py: 120 lines, 120 to send
…
Two ways to use it: as a command-line tool (this section), or as a Claude Code skill
(see Use it from Claude Code below). The skill runs sys1grep through npx,
so if you only use it through Claude Code you can skip the install here entirely and just set the API key.
Requires Node.js 20.16 or later. No other dependencies.
npm install -g @uehaj/sys1grep
sys1grep --help
To try it without installing, run it through npx (the first run downloads the package, later runs use the cache):
npx @uehaj/sys1grep -n -e "customer is angry or frustrated" tickets.txt
Then give it an API key from the TypeSafe console. Any one of these works:
export SYS1GREP_API_KEY=your-key # environment variable
# per user: ~/.config/sys1grep/settings.json (mkdir -p first; keep it at 0600)
(umask 077; echo '{"key": "your-key"}' > ~/.config/sys1grep/settings.json)
Variables already in the environment win; otherwise ~/.config/sys1grep/settings.json fills them in, one field per
variable:
| field | variable |
|---|---|
url | SYS1GREP_URL |
key | SYS1GREP_API_KEY (and TYPESAFE_API_KEY) |
model | SYS1GREP_MODEL |
opts | SYS1GREP_OPTS, as an array: ["--level", "strict", "-n"] |
summarizer | SYS1GREP_SUMMARIZER |
summarizerModel | SYS1GREP_SUMMARIZER_MODEL |
summarizerKey | SYS1GREP_SUMMARIZER_API_KEY |
Every field is optional. A value of the wrong type is an error, as is a file that is not JSON. A field the file does
not know only warns (on stderr, naming the field) and is ignored, so an older sys1grep keeps working once a newer one
adds a field. A file that holds a key and that others can read gets a warning. ~/.config/sys1grep/.env (NAME=value
lines) still works, below settings.json: a value in settings.json wins over the same variable there. A .env in the current
directory is never read: it may belong to a repository you just cloned, and could send your key elsewhere through
SYS1GREP_URL. For per-project settings, load a file yourself: node --env-file=.env "$(command -v sys1grep)" ....
TYPESAFE_API_KEY is accepted too when SYS1GREP_API_KEY is not set.
The --serve page's Settings dialog edits this settings.json too (see A search page).
SYS1GREP_OPTS holds options to apply on every call, read like the settings above. It is split on spaces and put in
front of the command line, so the command line wins: a later value counts, and --no-X turns a default flag off.
export SYS1GREP_OPTS='--level strict -j 8 -n'
sys1grep -e "payment failed" app.log # strict, 8 at once, line numbers
sys1grep --level loose --no-n -e "payment failed" app.log
The same defaults can live in settings.json as "opts": ["--level", "strict", "-j", "8", "-n"], one argument per
element (so a value may hold spaces). SYS1GREP_OPTS, when set, replaces them rather than adding to them.
A script calling sys1grep would pick these up too (grep dropped GREP_OPTIONS for that reason). Call it as
SYS1GREP_OPTS= sys1grep ... in scripts: the empty variable drops settings.json's opts as well.
The API is configured by exactly three settings: SYS1GREP_API_KEY (or TYPESAFE_API_KEY), SYS1GREP_URL and SYS1GREP_MODEL.
Any endpoint that speaks TypeSafe's POST /v1/systemone works. The key is sent to SYS1GREP_URL as is, so set the
two together. On the command line, --sys1-model=ID, --sys1-url=URL and --sys1-api-key=KEY override
the three. A key given this way shows up in ps and shell history, so prefer ~/.config/sys1grep/settings.json for it.
# OpenRouter
SYS1GREP_URL=https://openrouter.ai/api/v1/systemone SYS1GREP_API_KEY=sk-or-... sys1grep -e ...
# Vercel AI Gateway
SYS1GREP_URL=https://ai-gateway.vercel.sh/typesafe/v1/systemone SYS1GREP_MODEL=typesafe-ai/jev SYS1GREP_API_KEY=vck_... sys1grep -e ...
# A compatible server that needs no key: no Authorization header is sent
SYS1GREP_URL=http://localhost:8000/v1/systemone sys1grep -e ...
Every price sys1grep shows (--dry-run, the large-send warning, the --max-cost question, the summary line) is the cost
the endpoint reports (usage.cost), or else an estimate marked ~. The estimate's price per million input tokens: a local
URL is free (localhost, 127.0.0.1 or [::1], judged by the host as typed with no DNS lookup, and only for a URL you set
with SYS1GREP_URL or --sys1-url); a model in this table has its own price, jev-latest 0.042, clef 0.24 and
clef-flash 0.09 (USD); any other model is priced as Jev, 0.042. With another URL the estimate says whose price it is:
at TypeSafe's list price, at clef's list price or (local URL, free). --max-cost uses the same price, so it asks
about nothing on a local URL.
Local decision models. A model that serves the same POST /v1/systemone can be used instead of Jev, with no key and no
network, but both tried so far are less accurate than Jev: raise -t, tune with npm run judge, and expect more false
positives. Both also run one request at a time, so a local URL (localhost, 127.0.0.1, [::1]) defaults to -j 1;
a -j you give (on the command line, in SYS1GREP_OPTS or settings.json) still wins.
jeff-serve, 0.8B, base v1.2, zero-shot, MLX): works with
--sys1-url=http://127.0.0.1:8765/v1/systemone --sys1-model=jeff-latest. It answers a second request at once with
529 "The model is busy", and -j 8 runs out of retries. On npm run judge (51 lines, 10 cases, -j 1)
precision 0.68, recall 0.61, F1 0.64 at the default thresholds and F1 0.77 at -t 0.75 -T 0.85, against F1 0.96 for Jev
(a run of another day): not a drop-in. v1.2 is a community preview; adapters do not carry across versions.strands-decider serve, 2B, an M4 with mps):
SYS1GREP_URL=http://127.0.0.1:8000/v1/systemone with no key works as is (30 lines in 2.7 s). Two requests at once crash
its server (exit 134, a Metal assertion). On the same npm run judge set: precision 0.64, recall 0.94, F1 0.76 at the
default thresholds and F1 0.81 at -t 0.6 -T 0.45.A request carries at most 64 questions for every backend.
From source: git clone https://github.com/uehaj/sys1grep.git && cd sys1grep && npm install -g .,
or run it in place with node sys1grep.mjs ....
All examples run against tests/corpus.txt, a 51-line mix of server logs,
support tickets in English and Japanese, source code, SQL and small talk. Where a Japanese line would
otherwise show up in the output below, this section instead uses tests/corpus.en.txt,
the same 51 lines with the Japanese ones translated to English — the cross-language behaviour itself is
shown once, in Search across languages above.
For examples organised by job (on-call, triage, release notes, ...) and run on real data, see docs/use-cases.md.
$ sys1grep -n -e "customer is angry or frustrated" tests/corpus.en.txt
14:Inquiry from user Yamada: I want a refund, the item was broken
16:Inquiry from user Sato: I'm being charged for an order I never placed, it looks like fraud, please check urgently
18:I want my money back. The item arrived broken and customer service ignored me.
21:Your product ruined my weekend. Never buying from you again.
23:This is the third time I'm writing. Nobody has replied to my previous emails.
5 of 51 lines matched; 51 sent to Jev in 2 requests, 3054 input tokens, ~$0.000128
None of these lines contain the words "angry" or "frustrated".
-Q)-e asks whether a line states a meaning. -Q QUESTION (--question) finds lines that answer it instead,
so you can write the question as you would ask it:
$ echo "The job failed." | sys1grep -e "Did the job succeed?"
$ echo "The job failed." | sys1grep -Q "Did the job succeed?"
The job failed.
As a meaning, "Did the job succeed?" is not what the line says (0.08), so -e finds nothing. As a question,
the line answers it: no, it failed (0.83). A line that asks the question is close to it in meaning but
answers nothing, and for a yes/no question a line that denies it still answers it:
$ sys1grep -n -Q "whether the server is down" tests/intent.txt
5:The server is down.
6:The server is healthy and responding normally.
2 of 17 lines matched; 17 sent to Jev in 1 request, 1087 input tokens, ~$0.000046
Both the confirming line and the denying line match: each settles whether the server is down. Is the server down? does not match, because asking is not answering; -e "asking whether the server is down"
would match it instead. -Q X is shorthand for -e "the line answers: X", so it combines with -a / -v
/ ! and OR's with other terms exactly like -e.
-p$ sys1grep -n -p -e "customer is asking for a refund" -e "delivery address change request" tests/corpus.en.txt
14:Inquiry from user Yamada: I want a refund, the item was broken [0.99 0.01]
17:Inquiry from user Takahashi: I'd like to change the delivery address [0.01 0.99]
18:I want my money back. The item arrived broken and customer service ignored me. [0.96 0.01]
22:Can I change the delivery address for order #8821? [0.02 0.99]
4 of 51 lines matched; 51 sent to Jev in 2 requests, 4275 input tokens, ~$0.000180
The bracket shows one probability per meaning, in the order given. Use it to pick a threshold.
With --color (on by default in a terminal) the probabilities are colored against the thresholds:
green at or above -t, red below -T, yellow in between. Line numbers and file names use grep's colors.
$ sys1grep -n -e "network or remote connection failure" -v "a retry is happening or was attempted" tests/corpus.txt
4:2026-09-19 08:02:30 ERROR connection reset by peer while calling payment-gateway
6:2026-09-19 08:02:35 ERROR timeout after 5000ms waiting for payment-gateway
9:2026-09-19 08:10:44 ERROR DNS lookup failed for api.example.com
11:2026-09-19 09:00:00 ERROR SSL handshake failed: certificate expired
13:network unreachable: no route to host 10.0.0.5
30:except ConnectionError as e:
31: logger.error("upstream unreachable: %s", e)
7 of 51 lines matched; 51 sent to Jev in 2 requests, 5112 input tokens, ~$0.000215
Line 5, retrying payment-gateway request (attempt 2/3), is a network failure but is dropped by -v.
$ sys1grep -n -e "about economy, finance or markets" -a "the news is negative or a decline" -e "about weather" tests/corpus.en.txt
36:Today's weather is sunny, high of 28 degrees
43:Stock prices fell 3% after the earnings report missed expectations.
48:Tomorrow's forecast is rain so I'll bring an umbrella
3 of 51 lines matched; 51 sent to Jev in 2 requests, 5499 input tokens, ~$0.000231
The central bank raised interest rates is about finance but not a decline, so it is out.
$ sys1grep --level strict -n -e "a security risk or dangerous destructive operation" tests/corpus.en.txt
33:DROP TABLE sessions;
49:API keys must never be committed to the repository.
$ sys1grep --level loose -n -e "a security risk or dangerous destructive operation" tests/corpus.en.txt
11:2026-09-19 09:00:00 ERROR SSL handshake failed: certificate expired
16:Inquiry from user Sato: I'm being charged for an order I never placed, it looks like fraud, please check urgently
33:DROP TABLE sessions;
49:API keys must never be committed to the repository.
strict keeps only what the model is sure about. loose also pulls in the expired certificate and the suspicious-billing ticket.
$ sys1grep -r -n -e "customer is asking for a refund" tests/tickets/
tests/tickets/a.txt:7:Ticket #16: I want a refund, the item was broken.
tests/tickets/sub/b.txt:1:The customer wants a refund for the broken lamp.
$ sys1grep -rl -e "customer is asking for a refund" tests/tickets/
tests/tickets/a.txt
tests/tickets/sub/b.txt
-r walks directories in sorted order and skips .git, node_modules, .ssh, .aws, .gnupg, .kube,
.docker, binary files (a NUL byte in the first 8 KB, or a PDF; UTF-16 with a BOM is read as text), files
that usually hold secrets (.env*, .netrc, .npmrc, .pypirc, .pgpass, .git-credentials, *.pem,
*.key, *.p12, *.pfx, *.jks, *.keystore, id_rsa* and friends; names compared without case) and
generated files (*.map, *.min.js, *.min.css, package-lock.json, yarn.lock, pnpm-lock.yaml,
Cargo.lock, poetry.lock, composer.lock, Gemfile.lock, go.sum), symlinks and ~/.config/sys1grep/settings.json itself. Every line that is searched is sent
to the TypeSafe API (or wherever SYS1GREP_URL points), so point -r at a directory
you mean to scan. Inside a git repository, -r also skips what git ignores (.gitignore, .git/info/exclude, the
global excludes file), so build output and local files stay home; a tracked file is searched even if it matches.
A file named *.gz (a rotated log: app.log.1.gz) is read decompressed, as zgrep does, and printed by its
name on disk (app.log.1.gz:12:...); --include='*.gz' picks those, the binary sniff sees the decompressed
bytes, a gzipped secret (private.key.gz) is skipped like the plain one, and a corrupt .gz is an unreadable file.
Only gzip, only by the name.
A file named explicitly on the command line is always searched, even if it matches the skip list or is ignored;
so is a directory that git ignores, when you name it (sys1grep -r -e ... dist). -l prints each
matching file once, in the order matches are found, and works with or without -r. -c prints the number of
matching lines per file instead.
A meaning that restricts its matches to some kind of file can only match in such files. With -r and
git sys1grep, each meaning first asks Jev, in one small request, a yes / no per candidate, and a yes at 0.6 or
more narrows the files before anything else is sent. The rest is never read or sent. Each scope is reported on
stderr with Jev's answer, so a wrong one is visible:
$ sys1grep -r -e 'Python でリトライ処理を書いている箇所' .
sys1grep: scope: *.py *.pyi *.pyw (from "Python files: 0.94")
sys1grep: scope: 12 of 340 files
$ sys1grep -r -e '昨日変えた箇所で認証を扱っている' src/
sys1grep: scope: changed since 2026-09-25 00:00 (git commits; the mtime for files git does not have committed) (from "what was changed yesterday: 0.91")
sys1grep: scope: 3 of 120 files
tests/ test/ __tests__/ spec/ e2e/, test_*.py *_test.* *.test.* *.spec.* *Test.java *_spec.rb, and every *.rs: Rust keeps unit tests inside the file), database migrations (*migrat*, Flyway's
V1__*.sql), the README, the changelog (CHANGELOG* CHANGES* HISTORY* NEWS*), documentation (*.md *.rst *.adoc *.txt, docs/), source code (whatever is not documentation, so languages missing from the dictionary still
count) and logs (*.log *.log.N *.out *.err, logs/), by the path conventions of JS, Python, Go, Java, Ruby, Rust
and PHP. Several places are alternatives ("README か CHANGELOG に").origin/HEAD, main
or master), not pushed (@{upstream}..HEAD, else commits on no remote branch), and mine (user.email's
commits and the uncommitted files). Authors: the 30 with the most commits (git shortlog), each a candidate by
name and e-mail; a yes narrows to every file a commit of theirs touched. Outside a repository none of these is
asked, and a time goes by the mtime.-e A -e B still searches B in the files A's scope leaves out; within an AND term, and across
categories ("Python のテストコード"), the scopes intersect. Negated meanings (-v, !) are not asked. The
meaning is sent unchanged.git sys1grep)npm install -g also installs git-sys1grep, so git runs it as git sys1grep. Like git grep, it searches only
the files git tracks (ignored files and build output are never sent), and FILE arguments are pathspecs relative
to the current directory.
$ cd tests && git sys1grep -l -e "customer is asking for a refund" fixture.txt tickets
fixture.txt
tickets/a.txt
tickets/sub/b.txt
Without FILE it searches every tracked file under the current directory. The -r skip list (.env*, keys, ...)
applies even to tracked files. --include, --exclude and --changed-within filter the tracked files, those named
by a pathspec included. --changed-within reads the working tree's modification times, not git history: right after
a clone or a checkout, every file it wrote counts as just changed. For help use git sys1grep -h: git takes --help itself and looks for a man page.
--cached, --untracked and a <tree>... choose what is searched instead of the working tree, as git grep has
them (only one of the three at a time):
$ git sys1grep --cached -e "a retry is attempted" # staged, uncommitted changes included
$ git sys1grep --untracked -e "a retry is attempted" # tracked files plus untracked ones (.gitignore still applies)
$ git sys1grep -n -e "a retry is attempted" main v0.3.1 -- '*.py'
main:src/job.py:42: retry(job, times=3)
v0.3.1:src/job.py:40: retry(job)
A <tree> (a branch, tag, commit or @{u}) is any argument before -- that resolves as a revision; several may
be given, searched in the order given, each line prefixed with the name as typed (@{u}:path, not the branch it
resolves to). --changed-within needs the working tree: a blob (--cached, a <tree>) has no mtime of its own.
A <tree>'s own pathspec takes a glob too, same as elsewhere in sys1grep (git diff-tree against the empty
tree, not git ls-tree's own literal/directory-prefix match); --include / --exclude (by name) still work.
An old <tree> can hold a secret an ordinary file once carried and was later removed from: the skip list
drops files by name only, not by what changed since.
-g)-g (--gitlog) searches the commits of git log instead of files, one record each (hash, date, subject, body):
$ sys1grep -g -e 'a performance fix to the .mjs files today'
With one term, auto-scope becomes git log's own arguments, printed on stderr: a time becomes --since-as-filter
(--since on git before 2.37, and --until when the span has an end: yesterday, last week, last month), a language
a pathspec, an author or "mine" --author, "this branch" or "not pushed" a range. FILE arguments are pathspecs.
Each commit is a record, as with -z, so -M defaults to 8000. Not with -r or git sys1grep.
sys1grep -v "a timestamped server log line" mixed.txt # like grep -v
sys1grep -e "source code or SQL" -v "SQL" src.txt # code, but not SQL
cat app.log | sys1grep -e "the deploy failed or was rolled back"
-z)The unit of judgement is a line. That is right for logs and source, and wrong when one record spans
several lines. -z makes the unit a NUL-terminated record instead, exactly as in grep -z, so it pairs
with the tools that already emit records: git log -z, find -print0, xargs -0.
A proposition like "this commit changes user-visible behaviour" is true of a whole commit, not of any one line in it:
$ git log -z --format='%h %s %b' | sys1grep -z -n -e "the change alters user-visible behaviour" -v "documentation only"
7:21120e9 Revert "feat: ship the /semgrep Claude Code skill" ...
8:51ae333 feat: ship the /semgrep Claude Code skill
Matching records are printed NUL-terminated too, so pipe them through tr '\0' '\n' to read them.
File names (-l) and counts (-c) stay on newlines, as they do in grep. With -z, -n numbers records,
-A / -B / -C count neighbouring records, and --chunk counts records per request.
--unit=sentence-by-*)--unit=sentence-by-jev (or sentence-by-rule, below) judges each sentence instead of each line. The output is still lines, as in grep: every line a
matching sentence touches is printed, and on a terminal the sentence itself is in bold yellow (a regex match inside it, in grep's bold red).
Wrapped lines are joined before splitting, so a sentence that runs over several lines is judged as one.
tests/prose.txt wraps an English paragraph and a Japanese one:
$ sys1grep -n --unit=sentence-by-jev -e "the author admits they made a mistake" tests/prose.txt
1:I should have checked the input
2:before shipping, and that was my
3:mistake. Next time I will add a test
The sentence starts on line 1 and ends at mistake. on line 3; only that part is colored, not
Next time I will add a test, which is judged separately and does not match.
On a terminal, with both meanings and -C 3 for context, the colors show where each sentence starts and ends
inside a line: lines 3 and 9 are colored only up to the end of the matching sentence, and lines 4-7 are context (-):
-o prints only the matching sentences, one per line, as grep -o prints only the matching part.
-n then gives the line where the sentence starts. Japanese is joined without a space, as are Chinese,
Thai, Lao, Khmer, Myanmar and Tibetan, which do not put spaces between words:
$ sys1grep -n -o --unit=sentence-by-jev -e "the author admits they made a mistake" -e "customer is asking for a refund" tests/prose.txt
1:I should have checked the input before shipping, and that was my mistake.
8:先週買った掃除機が初日から動かないので返金してほしいです。
Without -o, -c counts lines and -A / -B / -C count lines, as usual. With -o they count sentences.
With -z, each record is split on its own and matching records are printed whole.
Jev finds a matching sentence inside a long line on its own, so --unit=sentence-by-jev is not needed for accuracy.
Use it to see which sentence matched, to get the sentences with -o, and when AND should hold within one
sentence: the expression is evaluated per sentence. For the same reason -v X alone prints every line
with at least one sentence that is not X; to find lines that are not X as a whole, leave --unit at line.
Japanese and Chinese entries often end without 。: a chat message, a support ticket, a memo line. Joining
them would glue separate entries into one "sentence". So with
--unit=sentence-by-jev sys1grep asks Jev about each unpunctuated break next to a script written without word
spaces: "does this line break end a sentence or entry, or is it a wrap inside a sentence?" It sends 30 lines
per request with one yes/no per break, and keeps the lines apart when the answer is 0.7 or more. On
tests/corpus.txt this keeps the four one-line Japanese tickets apart, so
--unit=sentence-by-jev finds the same refund requests (lines 14 and 18) as a line-by-line search, where the rules alone
merged the tickets and missed line 18. The extra requests cost about as much as one more meaning; use
--unit=sentence-by-rule to skip them. Breaks between English lines are never asked: joining them keeps a space,
and the full stop still ends the sentence.
Where a newline cannot be inside a sentence, lines are not joined: at a blank line, next to brackets or
; (JSON, code), and before a line starting with - * + # > " or a digit (list items,
headings, quotes, numbers, timestamps). So JSONL keeps one line per record and each line is split on its
own. Sentences are cut by Intl.Segmenter (Unicode UAX #29),
which splits at . ! ? 。 ! ? but also after abbreviations such as Mr.. Logs are not prose:
consecutive log lines that start with a letter, such as WARN ... after ERROR ..., get joined.
--unit=function)In code the question is usually about a function ("retries on network failure"), and one line of it rarely
says so. --unit=function judges each function and prints its lines, with -n giving file line numbers:
$ sys1grep -H -n --unit=function -e "retries on network failure" src/net.js
src/net.js:3:async function fetchWithRetry(url) {
src/net.js:4: for (let i = 0; i < 5; i++) {
src/net.js:5: try { return await fetch(url); }
src/net.js:6: catch { await sleep(2 ** i * 100); }
src/net.js:7: }
src/net.js:8:}
A function runs from a funcname line to the line before the next one, as git grep -W finds it; lines
before the first funcname line are one unit, and blank lines at a function's end are not printed. The
funcname lines are git's: the diff=<driver> attribute in .gitattributes, then diff.<driver>.xfuncname
in git config. Without one, sys1grep has its own rule for JavaScript/TypeScript (a top-level function,
class, or const / let / var bound to a function) and Python (def / class, nested ones too); a decorator line (@retry) starts
the function it decorates. Otherwise it uses git's default: a line starting with a letter, _ or $. git's own builtin drivers
(diff=python and so on) are not read, so a driver without an xfuncname falls back the same way:
$ printf '*.go diff=golang\n' >> .gitattributes
$ git config diff.golang.xfuncname '^(func|type)[[:space:]]'
-M defaults to 8000 characters here, as with -z; a longer function is judged on its first 8000.
--chunk counts functions, -A/-B/-C and -c count lines, as with the sentence units. -z, -g and
-o are refused.
--step-to)A question about code often comes in two steps: where does --summarize hand the lines to the tool, and then,
what happens when that fails. The "that" in the second step is what the first one found. Multi-step matching
answers both in one run: the expression before --step-to finds the start, sys1grep walks the calls from it,
and the expression after --step-to picks the functions it reaches that match. Jev judges the two
expressions; no score decides which call to follow.
$ sys1grep -e "--summarize hands the lines to the tool" --step-to "what happens when the tool fails to start or answer" sys1grep.mjs
A meaning may start with a dash, as the first one does, on any search and for -e, -a, -v and -Q: a value that starts with - is taken as the meaning unless it is an option (an option holds no space).
Each path prints as a tree: the hop (the number of calls from the start), file:line and the function's name,
: after an end and - after a function on the way, as grep marks a match and its context. A regex-only
start and end send nothing, which makes a free reachability search. In Python 3.6's json, the functions
that load reaches and that hold a raise:
$ cd /usr/lib64/python3.6/json && sys1grep -P '/^\s*def load\b/' --step-to -P '/\braise\b/' *.py
sys1grep: walk: 1 at hop 0, 1 at hop 1, 3 at hop 2, 1 at hop 3, 1 at hop 4, 1 at hop 5; stopped: no new unit
0 __init__.py-274-load
1 __init__.py:302:loads
2 decoder.py:334:decode
3 decoder.py:345:raw_decode
4 scanner.py-65-scan_once
5 scanner.py:28:_scan_once
--step-to is the start's, as in any search; every -e / -P /
-a / -v / -Q after it is the end's. A bare --step-to MEANING is --step-to -e MEANING and a bare
--step-to '/RE/' is auto-detected and becomes --step-to -P '/RE/'; the file names follow. Write the
explicit -P form for a regex end, as in the example above: it reads the same but names the option. A step
is a search, not a hop: one --step-to is two steps, whatever the hops between them.--edges the unit is a function (--unit=function) and an edge is a call: name( in
a function's body, its comments, docstrings and strings left out, links to every function of that name. A name
comes from the funcname line (the def under a decorator). --edges=FILE walks any other relation instead,
one edge per line, FROM_FILE:LINE<TAB>FROM_NAME<TAB>TO_FILE:LINE<TAB>TO_NAME; a line stands for the unit that
holds it. --step-reverse-to in place of --step-to walks the edges backwards, callee to caller: the same
arguments, with --hops and --edges, and not together with --step-to.--hops=N, --hops=M..N and --hops=M.. pick the
hops an end may be at (default 0..: a start that matches the end expression is an end at hop 0). A path that
reaches no end is left out. stderr says how many functions each hop reached and why the walk stopped: no new
unit, or --hops.--hops against the end expression in another. --max-cost asks again before the second, counting the
first. --dry-run answers every question 0, so it walks from every function the start expression could hold
for and shows that bound.-z, -g, -o, -c, -l, -A/-B/-C, --rank, --summarize or . adds the
end expression's scores to each function judged against it; prints nothing and exits 0 when there is an end.--dedup)Cost is proportional to the text sent, and machine-generated logs are mostly one skeleton with a different
id or number in it. --dedup masks ids, hashes, numbers, dates and times, paths and URLs, groups lines by
the result, sends one line per group and reuses its answer for the rest. What is sent is that line's
original text, and every line is printed as itself:
--dedup takes auto, always or never; a bare --dedup is --dedup=always. Default is never, for
now (#143): auto is held back until real-run stats say it should be the default. Every run still
estimates locally, before anything is sent, what folding every kind (the best case) would save, and prints
one stderr hint when that is worth at least twice the cost of --dedup's own question:
$ sys1grep -e "a request failed" app.log
sys1grep: 2040 units fold to at most 31 templates; --dedup=auto would save ~68 requests (~44k tokens)
--dedup=auto folds only when that estimate says it pays; a file that barely repeats sends every
line, asking nothing extra. --verbose / --dry-run print the decision and its numbers for every value.
$ sys1grep --dedup -n -e "a request failed" app.log
1:worker request 3fa9c1e27b failed: connection reset
2:worker request 88d0e41a5c failed: connection reset
4:worker request 0b7f2a9e13 failed: connection reset
3 of 6 lines matched; 4 sent to Jev (2 folded by --dedup, ~235 input tokens / ~$0.000010 saved, 21%) in 2 requests, 907 input tokens, ~$0.000038
Whether a value may be folded depends on the meaning: a number decides "disk usage is above 90%", a time
decides "happened at night". So --dedup=always (and a bare --dedup) first asks Jev, one small request per
meaning, which kinds of value could change a match, and keeps those kinds apart (one of the two requests above).
With -e "disk usage is above 90%" the same file keeps 95% and 12% apart and prints only 5:disk usage 95%.
--dedup=auto asks no such question (#152). It folds every kind and judges a sample of each template instead:
up to 5 members whose values differ, for each varying value the member holding its lowest, its highest and its
middle value. When every sample gets the same verdict, the members not sampled share the first sample's answers,
and the samples keep their own (-p prints each sample's own probability). When the verdicts differ, the rest are
sent in a second wave, and --max-cost is checked again before it. A split costs slightly more than never, at
worst one extra request per wave boundary, a few percent: one template of 600 members sent 21 requests where
never sent 20. A value only between the samples is missed: among usage 1 to usage 120, the meaning
usage 50 matches no sample, so auto prints nothing where never prints line 50. --verbose prints how many
templates agreed and how many units were sent again. --dry-run assumes every sample agrees, so its count is the
floor: 100 templates of 6 members show 10 requests and 300 of 600 lines, and a run where every template splits
sends all 600 lines in 20 requests.
Measured on real logs, with nothing kept: a 43,071-line system log folds into 550 templates (1.2% of the
bytes), install.log to 21.1%, a Claude Code transcript (jsonl) only to 56.8%. It is for machine-generated
logs; prose has no shared skeleton, and a meaning that reads a timestamp folds almost nothing. With -z or
--unit=sentence-by-* the records or sentences fold instead of lines.
After a search, the summary line says what the folding saved: (2 folded by --dedup, ~235 input tokens / ~$0.000010 saved, 21%) above. It estimates what the folded lines would have cost as requests of their own and subtracts
what the value questions cost. On a small file or on prose, the result can be negative.
To see how far your own logs fold before paying for a search, node scripts/dedup-measure.mjs FILE...
counts lines, templates and the share of bytes sent, offline, with the same masks; --keep=num,time shows
a meaning that keeps those kinds apart.
The request's other lines are each line's context (#9), and --dedup changes them, so a line near the
threshold can be judged differently than in a full pass.
--summarize)When many lines match, what you want is often the gist: what they say about the meaning you searched for.
--summarize pipes what sys1grep would print to claude -p --model haiku (no tools, no settings, no CLAUDE.md),
asks it to summarize the lines as they bear on the meanings, and prints its answer instead of the lines.
$ sys1grep -r -n --summarize -e "the API key is read from a file" .
The key is read in sys1grep.mjs:22 from ~/.config/sys1grep/.env, never from ./.env (sys1grep.mjs:18), ...
The expensive model reads only what Jev kept. Asking why ./.env is no longer read of this repository's
git log (168 commits), Claude's input fell from 22,059 tokens to 1,356, the total cost with Jev's from
$0.094 to $0.013, with the same answer (#69). It pays when the answer sits in a few lines.
-n, -A/-B/-C, -p and file names go in as they would print; colors never do. No match runs nothing (exit 1).SYS1GREP_SUMMARIZER picks the TOOL of a bare --summarize, SYS1GREP_SUMMARIZER_MODEL its model.-q, -l and -c print no lines, so they cannot be combined with it.SYS1GREP_OPTS it summarizes every search, so every match goes to TOOL's provider too;
--no-summarize turns it off for one search.--format=markdown or =html asks for those instead (… --format=html … > summary.html).
The answer prints as it comes, unchecked. In SYS1GREP_OPTS it is a standing preference, ignored without --summarize or --rank.--dedup, the TOOL gets what Jev got: each template's representative once, marked (×N like it),
not every line its answer was reused for.--summarize-prompt=TEXT adds your own instruction after the fixed one (how long, what to focus on):
$ sys1grep -r -n --summarize --summarize-prompt="3 lines or fewer, just which file to fix" \
-e "the API key is read from a file" .
It needs --summarize; empty TEXT is the same as leaving it out. It comes after the --format sentence,
so TEXT can override the format.
To decide after seeing the lines, --summarize=ask prints them as usual and then asks on the terminal
Summarize these matches? [y/N]. On y the lines just printed go to the TOOL of a bare --summarize: no second
search, nothing more sent to Jev. Enter or anything else ends there.
/dev/tty, so a stdin target still works. With no terminal, or stdout not one (a pipe,
$(...), a script), nothing is asked and nothing hangs.-y summarizes without asking, so -y in SYS1GREP_OPTS makes it summarize every time; -q and --dry-run never ask.--dedup, every matching line prints; the TOOL gets each template's representative, as with --summarize.y says so instead of sending.Other TOOLs:
$ sys1grep --summarize=llm -e "..." FILE # Simon Willison's llm, tools off (no -T given)
$ sys1grep --summarize=pi -e "..." FILE # pi --print --no-tools --no-session ...
$ SYS1GREP_SUMMARIZER_MODEL=qwen3.5:9b sys1grep --summarize=ollama -e "..." FILE
$ SYS1GREP_SUMMARIZER_MODEL=some-id sys1grep --summarize=lmstudio -e "..." FILE # model id from GET /v1/models
$ SYS1GREP_SUMMARIZER_MODEL=some-id sys1grep --summarize=http://localhost:8080/v1 -e "..." FILE # llama.cpp, vLLM, LocalAI, a gateway
ollama and lmstudio talk to a local OpenAI-compatible server (POST /v1/chat/completions) by fetch, no
CLI: the matching lines never leave the machine a second time. A plain http(s):// URL is any other
OpenAI-compatible server. All three need SYS1GREP_SUMMARIZER_MODEL: none has a default model.
SYS1GREP_SUMMARIZER_API_KEY goes as Authorization: Bearer to a URL TOOL only (never SYS1GREP_API_KEY,
which is Jev's). OLLAMA_HOST moves ollama's host, as it does for the ollama CLI itself.
--serve)--rank --format=html answers one question per run. --serve opens such a page (the search template, unless --template is given) with a search box, so you can rephrase
and narrow without rerunning the command:
$ sys1grep --serve -r src/
http://127.0.0.1:51234/?k=3f9e...
$ sys1grep --serve=8080 -y --level=strict -r src/ # a fixed port; the options given here are the page's first values
From a clone, npm run serve -- -r src/ is the same (-- keeps npm from reading the options; npm run serve -- --serve=8080 -r src/ fixes the port). With no target, --serve searches its own bundled sample corpus (examples/, #237) instead of reading an empty stdin, so every example button has something to find; name a target to search that instead. A folder named without -r is refused at launch, with the command that adds it (--serve: src is a directory: add -r to search it (sys1grep --serve -r src), #222).
The printed URL carries a token made fresh at launch (?k=...). Every route needs it, the page itself included: without
it (in the query, or in a header for the page's own requests) the server answers 404 to everything, so another local
account or process cannot read the page, its token, or a search's results merely by knowing the port.
The page is a set of cards under a blue header band: Target and Search conditions in one card with a large Search
button (Estimate cost and Stop under it) at its right, the examples and Options side by side below it (one column on
a phone), then the command line and Details. Target shows what every search reads: the files and folders given at launch
(npm run serve -- src docs/a.md), or with none what sys1grep falls back to: . with -r, every file git tracks
with git sys1grep, or, with neither, the bundled sample corpus (demo corpus (bundled): examples/, #237); while
-g (in Details) is on, it says git log's commits instead. It is shown only: the page cannot
change it, and a target in a request is ignored. Search conditions
holds the meaning fields and multi-step, Options rank and summarize,
and Details the other options; the results come below them. What they hold: the meaning fields (+ adds one; each takes -e, -P, -a, -v or -Q), rank, summarize, a multi-step
switch (on, it shows the end fields, --step-to and one or more meanings, and the edges, --hops and a reverse switch that makes it --step-reverse-to; off, they
leave the command and the search and keep their values), example buttons (searches from this README and
docs/use-cases.md, then multi-step walks; one fills the fields and the controls it needs, puts the rest back to their launch values, and waits for Search; EN / 日本語 beside the label switches their labels and
meanings, remembered in the browser), a
Details fold with the other options (--level, -t/-T, -C, -A/-B, --auto-scope, -n, -p, --dedup,
--unit, -o (shown only with a sentence unit), -j, --max-cost, --include, --exclude, --changed-within,
-g), and the
command line for the controls as they are now, with a Copy button; pasted in a terminal it runs the
same search. An option at its launch value is left out of what the controls add. off shows the matches in file order
as text, on shows the ranked cards; adds a right column: Search runs once, and once its results are on
screen the page summarizes those results (POSTs the text shown, rank's cards included, to the summarizer; no second
search, nothing extra sent to Jev, up to 200 KB as takes). Checking while results are
already on screen asks, in the page, whether to summarize those results as they are (#227): OK sends their text back
the same way; Not now leaves the box checked for the next Search. If a field or option changed since
those results came, the question says so. No results on screen, or multi-step on: no question. Estimate cost is . The first field is a question () by default. / (or , repeatable; a bare TEXT is a question) puts suggested-question buttons first in the examples row; pressing one fills the page, then Search runs it. The options in Details each have a "?" hint saying what they do. While a search runs, Search and Estimate are disabled and ends it (the child process is killed). The matches are colored as colors them (file names, line numbers, regex matches), the cards mark them with .
Each search runs sys1grep itself (the options given at launch, then the controls'), so everything the command line does
the page does, and only that. The server listens on 127.0.0.1 only, answers only to that host name, and the page never
sees the API key. The targets, --chunk, -M, --max-filesize, -y, --edges, --template, the
API settings and --summarize's TOOL are fixed at launch: the page can narrow what was given but not widen it (--chunk,
-M and --max-filesize still reach the Settings dialog's default options). A search over --max-cost is refused at
once (--serve has no /dev/tty to ask -y's question on, or launch with -y to let it through unasked); the error
carries the same estimate --dry-run would show, and a button next to it reruns that one search, once, with
--max-cost just above the estimate (not a change to the Details field).
The meanings come from the page, so -e -P -a -v -Q --step-to --step-reverse-to and -l -c -q -o -z --unit=zero -H -i --format --color --dry-run --verbose are refused at launch.
Settings edits ~/.config/sys1grep/settings.json (see Install): url, model, the key, opts (one argument
per line) and the summarizer's three. Save writes the file, and the next search uses it, as does every later sys1grep
run. Next to each field the page says where the value a search uses comes from, and says so when an environment variable or a
launch option wins over the file, since then saving does not change the search. A key comes back only as saved / not
saved; type a new one to replace it, or tick remove. A --sys1-api-key inside opts, and a url's userinfo, come back
masked as ***, the same way --verbose masks them; opts refuses a target, a flag the page itself refuses at launch,
or --sys1-url / --sys1-api-key (their own fields), and url and a URL summarizer must be http(s). The save is a
POST with the page's token and this request's own Host as its required Origin, and the file is written whole (0600,
its directory 0700 when created) and renamed into place, so a reader never sees half of it.
--rank)Matches print in file order, as grep prints them. With many, --rank prints the results best first, each under a
numbered header. A result is a match with its -A/-B/-C lines; matches whose context touches are one result.
$ sys1grep -n -C1 --rank -e "the refund was refused" tickets/
1. tickets/b.txt
tickets/b.txt-2-Order #1234, placed 2026-08-01.
tickets/b.txt:3:Your refund was declined: the order is older than 30 days.
tickets/b.txt-4-Please contact support.
2. tickets/a.txt
tickets/a.txt-11-Order #88 arrived.
tickets/a.txt:12:Refund request received.
tickets/a.txt-13-We will check it.
--rank is --rank=jev: after the search, Jev is asked of each result, lines and context together, whether it
is relevant to the meanings that are not negated. One more question per result, --chunk results (at most 64) to a request;
--dry-run / -i show an upper bound, since which results there are is known only after the search.--rank=match sorts by each result's highest match probability, with no request. It is the answer to a yes/no
question on one line, so clear matches sit close together, and the context is not read.-p puts the score on the header (1. [0.96] tickets/b.txt). -l lists the files by their best result.--format=markdown writes a ## 1. tickets/b.txt heading and a fenced block per result; --format=html one
self-contained HTML document from a template (below), light and dark, with a relevance bar per result. The lines
are as they print, escaped; what matched is in <mark> (the regex matches, else the matching sentences, else the whole
matching line; context lines are not marked; --color=never turns it off).--summarize gets the results in ranked order; with --dedup a representative is one result.! or -v alone ranks nothing), and cannot be combined with -c, -o or -q.
--no-rank turns off an earlier one, from SYS1GREP_OPTS say.A failed job's last error usually names a symptom (214 accounts out of balance ... day-close aborted), and the
other ERROR / WARN lines are retries that recovered. The cause is often one plain INFO or DEBUG line far
above. Asking for the symptom, or -Q "why did the job fail", finds that noise: on the 30,000-line log of #156 it
returned only recovered ERROR lines. Ask by kind of cause instead, and rank the results:
$ sys1grep -n --dedup --level strict --rank \
-e "a component, writer, job or feature was paused, disabled, skipped or put on hold" \
-e "a configuration, endpoint, region, credential or data source was changed or switched" \
-e "an input, file or table was empty, missing, or had fewer records or columns than expected" \
-e "a fallback or default value was used instead of the real one" \
job.log
Measured on 24 generated logs of failed jobs, six per kind of cause, 10,008 lines each, with these four meanings
(npm run cause-eval, rows in tests/cause-results.tsv; an earlier version of the script wrote its 24 rows, and
the top 2 jev column is filled only in the two rerun rows). Each search returned 366 to 464 results. The
--rank=match column is not a separate run: it is recomputed from the -p columns of the --rank run, since
--rank=match scores a result by the highest of them. That recomputation was checked against a real --rank=match
run only on config-5, where the cause was not a result. A cause "not matched" may have been judged below strict or
folded by --dedup into a like line; the rows do not tell the two apart.
| cause | cause is result 1, --rank | cause is result 1, --rank=match | otherwise |
|---|---|---|---|
| a configuration, endpoint or credential switched | 5 of 6 | 5 of 6 | 1 not matched |
| an input empty or short | 2 of 6 | 2 of 6 | 2 at results 62 (match: 343-371) and 81 (match: 191-219); 2 not matched |
| a default used instead of the real value | 6 of 6 | 5 of 6 | match: 2-9 (one result above it, seven tied with it at 0.90) |
| a line that never came (a start with no finish) | 0 of 6 | 0 of 6 | the start line never a result |
A log cost about $0.088 (2.1M input tokens, 643 to 646 requests) and 16 to 17 s. --rank (jev) adds one question
per result, about 3% more input tokens than --rank=match (one log run both ways; not recorded in the TSV). It put
the cause higher than --rank=match on three logs and lower on none.
When the top results look unrelated (rotations, refreshes, the final error itself), the cause is likely one of these:
stock snapshot downloaded: 1204 bytes, input directory ... holds 2 files,
resolved db-primary.internal to 10.0.4.22 (replica-2). Nothing in the line says it is wrong, so no hypothesis
holds, nor did a meaning written from the final error
(-e "the job connected to a read-only replica instead of the primary database"). Search for the thing the error
names instead (-P '/snapshot|replica/') and read the numbers.alias products switched to products-20260927, whose 09-28
counterpart never came. A meaning for it
(-e "a worker, step or upload started but never reported that it finished") put the final error first, not the
start, and --dedup can fold one start into a like one, since only the missing finish tells them apart. Compare
starts with finishes by eye or with grep.--rank; on two logs
where it never matched, the top result scored 0.88 and 0.91.The follow-ups behind the first two points (key lines judged alone in a 31-line window, the meanings written from
the error or for a missing finish) are in the description of PR #192; tests holds no way to rerun them.
Every searched line goes to Jev (TypeSafe, or whatever SYS1GREP_URL names), and with --summarize the matching
lines go to its TOOL's provider as well. Build logs and job logs often hold tokens, passwords and customer data.
--template)--rank --format=html and --summarize --format=html fill in a template. Four come with sys1grep: default, print (black on white, serif,
for paper and PDF), search (a results page like a web search engine's) and terminal (dark monospace).
--template=NAME picks one; SYS1GREP_TEMPLATE, in the
environment or ~/.config/sys1grep/.env, sets the default.
$ sys1grep -r -n -C1 --rank -p --format=html --template=print -e "the refund was refused" tickets/ > refunds.html
A template is one HTML file. The part between <!--result--> and <!--/result--> is repeated for each result;
what is before and after it is written once. sys1grep replaces these, escaped, and leaves any other {{…}} as
written:
| placeholder | where | value |
|---|---|---|
{{title}} | anywhere | sys1grep: and the meanings |
{{query}} | anywhere | the meanings, as in "refund" and not "policy" |
{{count}} | anywhere | the number of results (with --summarize, of matching lines) |
{{answer}} | anywhere | --summarize's answer; empty with --rank |
{{rank}} | per result | 1, 2, … |
{{score}} | per result | the score as -p prints it (0.96); empty without -p |
{{score_pct}} | per result | the score as a whole number 0 to 100, set even without -p (for a bar: style="--s:{{score_pct}}") |
{{file}} | per result | the file name; empty when one file is searched |
{{lines}} | per result | the result's lines as they print |
--template=NAME reads ~/.config/sys1grep/templates/NAME.html if it is there, else the bundled one, so your
copy of default replaces it. A value containing / or ending in .html is a file path. To make your own, copy
the bundled ones and edit:
$ sys1grep --install-templates # "copied PATH" for each file
$ cp ~/.config/sys1grep/templates/default.html ~/.config/sys1grep/templates/team.html
$ sys1grep -r --rank --format=html --template=team -e "..." src/ > out.html
$ sys1grep --template=list # one name a line, "(user)" after those under ~/.config
{{score_pct}} is set even without -p, so a bar can show the relevance without printing the number.
With --summarize --format=html the TOOL is asked for plain text, and that answer, escaped, goes into {{answer}}
once it has finished; the part between <!--result--> and <!--/result--> is not written. A template for
--summarize therefore needs {{answer}} outside that part (the bundled ones have it; with --rank it is empty).
$ sys1grep -r --summarize --format=html --template=print -e "why the refunds were refused" tickets/ > why.html
A template is trusted local input: a path you give, or a symlink in the templates directory, is read as it is; only the results put into it are escaped.
--install-templates never overwrites a file that is already there (it prints kept for it). A missing
template, or one without exactly one <!--result--> before one <!--/result-->, is exit 2 naming the file.
--template on the command line needs --format=html with --rank or --summarize (not -l); SYS1GREP_TEMPLATE is simply unused elsewhere.
--template=list and --install-templates take no other arguments and are refused in SYS1GREP_OPTS.
There is a Claude Code skill that runs sys1grep for you: describe what you are looking for in plain words
and it builds the expression, runs the search and reports file:line hits. It is published in the
uehaj/uehaj-marketplace marketplace as the uehaj plugin.
claude plugin marketplace add uehaj/uehaj-marketplace
claude plugin install uehaj@uehaj-marketplace
No separate install of the command-line tool is needed: the skill runs a version it pins,
through npx -y @uehaj/sys1grep@VERSION, not the sys1grep on your PATH. Only the API key has to be set (see Install).
Then, inside Claude Code:
/uehaj:sys1grep customer is asking for a refund tickets/*.txt
/uehaj:sys1grep a fix that shipped without a test git log --oneline -200
Claude Code installs plugins, not single skills. If you want just this one skill, the
skills CLI copies it into ~/.claude/skills/ and it is invoked as /sys1grep:
npx skills add uehaj/uehaj-marketplace --skill sys1grep -a claude-code -g
The skill writes the meaning in English, picks -e / -a / -v for OR / AND / NOT, adds -n, narrows large
directories to files worth paying for, and re-runs with --level loose or strict when the first result looks off.
The API key and endpoint are read the same way as on the command line (SYS1GREP_API_KEY, ~/.config/sys1grep/settings.json, ~/.config/sys1grep/.env).
usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION|-P /RE/FLAGS [-a MEANING] [-v MEANING]... [FILE...]
sys1grep [OPTION]... -e START1 [-e START2]... --step-to END1 [-e END2]... [FILE...]
-e MEANING lines matching this meaning (several -e are OR'd); MEANING is always sent to Jev, never
read as a regex — a /RE/flags-shaped value is refused, with a hint naming -P. A meaning
that really starts and ends with / gets through with a leading space (-e ' /cat/')
-P '/RE/FLAGS' a regex term, OR'd like -e; matched locally, no request; -a/-v after it AND/AND NOT
onto it, same as after -e; -P '!/RE/' is not allowed, negate with -v '/RE/'
-Q, --question QUESTION lines that answer QUESTION, not lines asking it; the same as
-e "the line answers: QUESTION" (see "Lines that answer a question" above)
-a MEANING AND onto the preceding -e/-Q/-P term. -e A -a B -e C = (A and B) or C
-v MEANING AND NOT onto the preceding -e/-Q/-P term. -e A -v B = A and not B
At the front it is a bare negation. -v B = not B (like grep -v)
!MEANING a leading ! negates just that meaning, in -e / -Q / -a / -v alike (not -P)
-e A -e '!B' = A or not B. -a '!C' is the same as -v C
--level=LEVEL strictness preset, sets both thresholds (default normal)
loose : -t 0.3 -T 0.7 catch more, accept some noise
normal : -t 0.5 -T 0.5
strict : -t 0.7 -T 0.3 only confident matches
-t THRESH positive threshold: match when probability >= THRESH (overrides --level)
-T THRESH negative threshold: "not X" when probability < THRESH (overrides --level)
with -t 0.6 -T 0.3 a line at 0.3..0.6 matches neither X nor not-X
-r recurse into directories (current directory when FILE is omitted);
skips .git, node_modules, binary files, likely secrets, generated files and what git ignores
--cached git sys1grep only: search the index instead of the working tree, as git grep --cached
--untracked git sys1grep only: also search untracked files (.gitignore still applies), as git grep --untracked
<tree>... git sys1grep only: a branch, tag, commit or @{u} before -- searches that revision's tree
instead, as git grep does; output is prefixed <tree>: with the name as typed
--include=GLOB, --exclude=GLOB with -r and git sys1grep, only files whose name matches GLOB, or not
(with -r a file named on the command line is always searched; git sys1grep's pathspecs are filtered)
--changed-within=WHEN with -r and git sys1grep, only files modified within 30m / 2h / 7d / 2w, since a date
or date-time, today, this-week or this-month
--no-auto-scope do not narrow those files by what a meaning says about them (see Scope from the meaning);
--auto-scope turns it back on
-g, --gitlog search git log's commits, one record each, instead of files; FILE arguments are pathspecs
(see "Scope from the meaning"). Not with -r or git sys1grep
-l print only the names of files with a match, not the lines
-H, --with-filename prefix file names even for a single file; --no-filename never prefixes them
-A NUM print NUM lines of trailing context after each match (context lines use - as separator)
-B NUM print NUM lines of leading context before each match
-C NUM print NUM lines of context before and after (-A NUM -B NUM)
-c print only a count of matching lines per file (like grep -c)
-q, --quiet print nothing, stop at the first match; exit 0 on a match, even after an error (like grep -q)
--chunk=LINES lines per request (default 30; a request also carries at most 64 questions, so fewer lines
with 3 or more meanings)
Lines in one request are each other's context, so a small chunk changes verdicts
on ambiguous lines, not just speed
-j N concurrent requests (default 8; 1 for a local URL: localhost, 127.0.0.1, [::1])
-n print line numbers
--unit=UNIT the unit of judgement: line (default), zero, sentence-by-jev, sentence-by-rule or function
-z, --null-data --unit=zero: judge NUL-terminated records instead of lines, and print them NUL-terminated (see "Records that span several lines" above);
with --unit=sentence-by-*, each record is split into sentences
--unit=sentence-by-jev|sentence-by-rule judge each sentence instead of each line (see "One sentence at a time" above)
--unit=function judge each function and print its lines (see "One function at a time" above)
--step-to EXPRESSION print the paths from the functions the expression before it matches, along the
calls, to the functions the expression after it matches (see "From one function to another" above)
--step-reverse-to EXPRESSION --step-to walking the edges backwards, callee to caller
--edges=FILE walk the edges in FILE instead of the calls
--hops=N|M..N|M.. the hops an end may be at (default 0..)
-o print only each regex match, one per line, as grep -o (a line only meanings matched prints
whole); with --unit=sentence-by-*, only the matching sentences
-p print each meaning's probability at the end of the line
--dry-run send nothing; print the settings the search would run with (and their source, when not the
command line), each file searched and each request with its questions
--verbose print the same to stderr while searching
-i, --interactive show what --dry-run would send, and search only after y on the terminal
-M NUM, --max-columns=NUM send at most the first NUM characters of a line (or record, with -z); still
searched and judged, just not past that cutoff (default 2000, 8000 with -z or --unit=function)
--max-filesize=SIZE size every target first (K/M/G, default 10M); over it, skip it outright, like rg's own
--max-filesize (-y does not affect it). stdin is sized once read, and skipped the same way if
it is over; a --cached / <tree>: target is a blob, sized from its content; -g's commits stay
out of this (each is already bounded by -M when sent)
--max-cost=USD price the input about to be sent; over it (default 1), ask to continue on the terminal; -y
answers yes without asking; no terminal and the limit exceeded is exit 2, unchanged by -q
(scripts pass -y); -i already asks unconditionally and earlier, so this does not ask again
-y, --yes answer that question yes without asking
--dedup[=auto|always|never] judge one line per template and reuse its answer for the rest (default never,
for now; see "One line per template" above)
--cache[=on|off|only] answer a request sent before from ~/.config/sys1grep/cache, free; only: never send,
a request not cached is exit 2 unless -q already found a match (default off,
SYS1GREP_CACHE; see "Sending less");
--cache=clear (alone) empties it
--rank[=jev|match] print the results (a match with its context) best first, under numbered headers; jev
(bare --rank) asks Jev of each result, match sorts by its best match probability (see "Best first"); --no-rank: file order
--summarize[=TOOL] pipe what would print to TOOL (claude by default, or SYS1GREP_SUMMARIZER) and print its
summary instead; --summarize=ask prints the lines, then asks on the terminal; --no-summarize
turns it off (see "A summary instead of the lines")
--summarize-prompt=TEXT your own instruction, added after the fixed one; needs --summarize
--format=FORMAT plain (default) / markdown / html: --rank's output, or asked of --summarize's TOOL
--serve[=PORT] serve a search page on 127.0.0.1 instead of searching; --suggest=Q:TEXT / e:TEXT adds
suggested-question buttons (see "A search page (`--serve`)")
--template=NAME the document --rank or --summarize --format=html writes: ~/.config/sys1grep/templates/NAME.html, else the
bundled one (default, print, search, terminal), or a file; default SYS1GREP_TEMPLATE, else default.
--template=list prints the names (see "HTML templates")
--install-templates copy the bundled templates to ~/.config/sys1grep/templates, keeping existing files
--color[=WHEN] auto (default: color when stdout is a terminal) / always / never; bare --color means auto
file and line number use grep's colors; with -p, probabilities are
green at or above the positive threshold, red below the negative one,
yellow in between. NO_COLOR is honored
--sys1-model=ID, --sys1-url=URL, --sys1-api-key=KEY
the API settings, overriding SYS1GREP_MODEL, SYS1GREP_URL, SYS1GREP_API_KEY
-h, --help this help (Japanese when the first set of LC_ALL / LC_MESSAGES / LANG starts with ja)
-V, --version print the version and exit
Without FILE, stdin is read. With several files, output is prefixed with file:.
Exit codes follow grep: 0 matched, 1 no match, 2 error (bad arguments, unreadable file, API failure).
Exit 1 after lines were sent prints one line on stderr: the highest probability, its line, the option that
loosens the threshold (unless even that would not match), and -p -t 0 (-T 1 too with a negation) to see every
probability (sys1grep: no line reached 0.5 for "…"; the highest was 0.42 (app.log:118). ...).
Not with -q, --summarize (except --summarize=ask), --step-to or --step-reverse-to, nor when only a negated meaning kept lines out.
-e and -P each start an OR term, the first always a meaning, the second always a regex. -a and
-v extend the previous term with AND and AND NOT.
A leading ! on a meaning negates just that meaning (quote it, ! is history expansion in most shells);
a leading ! on a -P value is refused instead — negate a regex term with -v '/RE/'.
| command | means |
|---|---|
-e A -e B | A or B |
-e A -a B | A and B |
-e A -v B | A and not B |
-e A -a B -v C -e D | (A and B and not C) or D |
-e A -e '!B' | A or not B |
-v B | not B |
state as an object {"L000": "line 1", "L001": "line 2", ...},
and one noul (yes/no probability) question per line × meaning goes into the same request.-j; 1 for a local URL). Output is printed in file order.Lines sent together are each other's context: Jev judges a line against what the rest of the chunk shows
is normal in the file. Clear matches and non-matches hold, but ambiguous lines can move. Where the chunk
boundaries fall barely matters (moving them by 15 lines at --chunk 30 flipped 1 line in 200 of a log), but
judging a line with little of its file around it does: --chunk 1 flipped 18 of the same 200, and those
solitary verdicts were the less reliable ones (#9). So --chunk changes results, not just speed, and a
very short input is judged with little context whatever --chunk says. One line at a time is also slow
(30 lines in one request take about 0.2 s, one line at a time about 7 s).
Very large chunks start losing lines near the threshold, hence the default of 30.
Probabilities drift by about ±0.05 between runs. Use -p when tuning thresholds.
Pricing is $0.042 per million input tokens (September 2026). A 30-line chunk with two meanings is about 3,000 tokens. Throughput is bounded by the rate limit of 1,200 requests per minute, roughly 36,000 lines per minute at the defaults.
tests/ holds an LLM-as-judge test. Each of the 10 cases in tests/cases.json runs against
tests/corpus.txt (51 lines). Claude (claude -p) decides which lines truly match each meaning;
the runner evaluates the boolean expression on those verdicts and compares with sys1grep's output,
reporting precision and recall. It also sweeps -t × -T in 0.05 steps and reports the best pair.
npm run judge [-- --model sonnet] [--rejudge] # runs tests/judge.mts; .mts needs Node 23.6+
Judge verdicts are cached in tests/verdicts.json; later runs do not call the judge.
The result is written to tests/report.md. Latest: precision 0.94, recall 0.98.
The judge is one of several test entry points. npm run test:offline checks sys1grep itself against a fake Jev
(tests/fake-jev.mjs): no key, no network, the same result every run (OFFLINE_ONLY=PATTERN runs only the matching
sections). npm test adds tests/check.sh against real Jev. CLAUDE.md lists every entry point with what it
checks and what it needs.
SYS1GREP_URL points). Do not run it over files you would not upload there.-v X prints them and -e X never does.-z or --unit=function; -M sets it).Some options were left out because a standard tool in front of sys1grep already does the job. These are the combinations.
--paragraph?Join each paragraph into one line first. fmt joins the lines of a paragraph and keeps the blank lines
between paragraphs:
fmt -w 100000 essay.txt | sys1grep -n -e "the author admits they made a mistake"
Each output line is then a whole paragraph, and -n counts the lines of fmt's output, not of the file.
fmt puts a space where it joins two Japanese lines; Jev reads through it. To judge sentences across
wrapped lines you need neither: --unit=sentence-by-jev already joins them.
Take the text out with jq and end each message with NUL, then judge records with -z:
jq -j '.content + "\u0000"' chat.jsonl | sys1grep -z -n --unit=sentence-by-jev -e "the customer is asking for a refund" | tr '\0' '\n'
Adjust .content to where your log keeps the text. Each message becomes one record, so a sentence never
runs into the next speaker's message, and the JSON punctuation is not sent. -n numbers messages. The file
also works as is, one JSON object per line; jq just gives cleaner units.
--record-separator?Turn the separator into NUL and use -z. For records separated by a ---- line:
perl -0777 -pe 's/\n----\n/\0/g' notes.txt | sys1grep -z -e "a decision was made" | tr '\0' '\n'
awk '/^----$/ { printf "%c", 0; next } { print }' notes.txt | sys1grep -z -e "a decision was made" | tr '\0' '\n'
NUL is what git log -z, find -print0 and xargs -0 already emit, so -z covers them directly (#6).
A general separator would bring escaping, multi-byte and regex-or-literal questions for a case one line of
perl or awk handles. The awk form works with the BSD awk on macOS, which does not take a
multi-character RS.
MIT
--cache=clear--dry-run sends nothing and prints the settings the search would run with, the
files, how many lines each would send and every request with its questions. Its last line estimates the input
tokens and the price (~3178 input tokens, ~$0.000133; within about 10%; see "Other endpoints" for whose price). -i shows the
same totals on the terminal and sends only after y.
Before the bulk of requests, every target is sized (--max-filesize, default 10M); one over it is skipped
outright, like rg's own --max-filesize, named on stderr (-y does not affect it). A .gz is sized
decompressed (zlib stops at the limit, so an oversized one is never inflated in full). stdin is sized once it
is read (it is already in memory), and skipped the same way if it is over; a --cached or <tree>: target
is a blob rather than a file, sized from its content (already read in one git cat-file --batch call, not a
fresh git process per blob); -g's commits stay out of this, since each is already bounded by -M when it
is sent. The input about to be sent is priced too (--max-cost,
default 1 USD); over it, one question asks to continue on the terminal, -y answers it yes, and without a
terminal it is exit 2 (this applies with -q too — scripts pass -y).
With -r or git sys1grep, a term no regex narrows that is about to send more than 10,000 lines gets one
stderr line with the same totals, naming the term (sys1grep: sending 214,913 of 231,502 lines from 1,247 files (~9.1M input tokens, ~$0.38); the term "…" has no regex to narrow it. Add -a '/RE/' to it, …). The search goes
on; -q silences the line, -y does not.-rgit sys1grep-i--dry-run[scope]--include-rgit sys1grep--no-auto-scope--auto-scope--dedup-p-qranksummarize--summarizesummarize--dry-run-Q--suggest=Q:TEXT--suggest=e:TEXT--suggest TEXT--color=always<mark>