Ask the stream a question.
GARules wait for an attack you already know how to describe. A hunt goes looking for one you don’t yet: the address behind most failed sign-ins, the identity reading more secrets than its peers, the client software only one session ever used. TillHunt searches your workspace’s security events in the same grammar TillTell rules use, so a question that finds something becomes a rule without rewriting it. Open it under Shield → TillHunt.
The search bar
A search is a list of field:value terms. Every term must hold. Put a dash in front to require that it doesn’t:
action:auth.login_failed -outcome:success actor.ip|cidr:203.0.113.0/24| Write | Matches |
|---|---|
action:auth.login | The whole value, ignoring case. |
-actor.country:ZA | Events where the term does not hold, including events without the field. |
actor.user_agent:*curl* | * stands for any run of characters and ? for one. |
target.name:"prod db" | Quote a value with spaces. Inside quotes, \" is a quote and \\ a backslash. |
actor.ip|cidr:10.0.0.0/8 | A modifier after a pipe changes the comparison: contains, startswith, endswith, re, cidr, gt, gte, lt, lte and exists, plus cased to respect case. |
actor.session_id:null | The field is missing. Bare true, false and numbers are read as those types; quote them to search for the text. |
Modifiers work exactly as they do in a rule; the rule guide has the full list. A search holds up to 32 terms and 2,000 characters. Press / to jump to the search bar and Enter to run. An empty search reads every event in the range.
What every event carries
| Field | Holds |
|---|---|
source | The Till product or app that recorded the event |
action | What happened, like auth.login_failed |
outcome | success, failure or denied |
trust | server for server-side evidence, client for what an app reported |
actor.type | user, service or unknown |
actor.id | Who did it |
actor.ip | From which address |
actor.country | Two-letter country code |
actor.asn | Network number |
actor.session_id | The session that did it |
actor.user_agent | Client software |
actor.geo.lat | Latitude, when the source knows it |
actor.geo.lon | Longitude, when the source knows it |
target.type | What was acted on |
target.id | Which one |
target.name | Its name at the time |
project_id | The project, for app events |
event_id | The event itself |
raw.host.name | The host, for endpoint events |
raw.host.id | The host's id in TillShield → Endpoints |
raw.host.os | linux, macos or windows |
raw.collector | The agent collector that saw it, like exec or event_log |
raw.process.name | The program, for endpoint process events |
raw.process.path | Its full path |
raw.process.sha256 | The SHA-256 of its binary |
raw.process.user | The account it ran as |
raw.process.command_line | Its arguments, with secrets removed on the host |
raw.process.parent_name | The program that started it |
raw.process.signing_id | Its code-signing identity, on macOS |
raw.detail.<field> | What else the endpoint agent recorded, like kind or port |
raw.<field> | Anything else the source recorded |
The What the stream holds panel counts the last seven days of events by source, action and outcome, so you can see what’s normal before asking what isn’t.
Anything richer
The search bar only joins terms with “and”. For “or”, lists of values, 1 of and all of, switch the query to YAML and write selections and a condition, as in a rule’s detection. Switching a search to YAML writes the search out for you. This is the detection behind the starter hunt for role and key changes:
roles:
action:
- iam.role_changed
- iam.member_added
keys:
action:
- iam.api_key_created
- iam.token_created
- repo.key_added
condition: 1 of them
A hunt is held to the same matching budget as a workspace rule: 1,000 value comparisons per event and 2,000 compiled regex instructions. A hunt’s text is at most 16 KB. Check compiles a query and shows its cost without reading any events.
Counts, groups and events
- Range. Pick one from an hour to 400 days. Click a bar of the histogram, which splits the range into 48 slices, to zoom into it.
- Group by up to 5 fields to count matches per value, with when each was first and last seen. Sort by most common to see who dominates, or rarest to see who stands out. Past 1,000 distinct groups, the 500 most common and 500 least common come back, with the total.
- Events lists the newest 200 matches. Expand one to read the whole event.
- Pivot by clicking any value: add it to the search, exclude it, start a new search for it, group by its field, or copy it. Each pivot writes an exact-match term and runs again.
Events from a labelled drill are left out unless you tick Include drills, and the overview never counts them.
It says what it read
A hunt first asks the stream for events that could match, then decides each one with the TillTell rule matcher. What a hunt finds is exactly what the rule made from it would fire on. Each run is bounded:
| Limit | Value |
|---|---|
| Candidate events checked per run, newest first | 10,000 |
| Time spent matching per run | 3 seconds |
| Matching events returned | 200 |
| Range | 1 hour to 400 days |
| Group-by fields | 5 |
| Runs per person | 30 per 5 minutes |
| Checks per person | 120 per 5 minutes |
When a run stops short, it says so. It shows how many candidates it checked out of how many there were and how far back it reached, and its counts and groups cover only that stretch. Scan further back continues from where it stopped. A run that checked every candidate says that too.
source or action term to make a broad hunt fast. A query the stream would spend more than 20 seconds on, or that would read more than a billion events, is refused with a request to shorten the range or narrow the search.Questions worth asking again
Save a hunt with a name and a hypothesis: what you expect to find and why it matters. A saved hunt keeps its query, group-by, tags, range and whether it includes drills, and is shared with the workspace’s owners and admins. It records when it last ran and how many events matched, but only for runs exactly as saved. A workspace keeps up to 200 saved hunts, each with a name no other hunt in it uses, whatever the case. ⌘/Ctrl + S saves changes.
The Starters panel holds questions worth asking on the first day:
| Starter | Hypothesis |
|---|---|
| Failed sign-ins by address | A few addresses account for most failed sign-ins: guessing or stuffing that stays under the alert line. |
| Who reads secrets, and how much | One identity reading far more secrets than its peers is collecting them. |
| Rare client software on sign-in | A sign-in from client software nobody else uses is a script or a stolen session. |
| Role and key changes | New admins and new keys outside change windows are how attackers keep access. |
| Sign-ins by country | A country seen once in a month of sign-ins deserves a look. |
Keep watching for it
When a hunt finds something that should never happen quietly again, choose Make it a rule…. TillHunt runs the hunt once more and drafts a TillTell rule with the same detection and ATT&CK tags, at the level you pick. Its tests are made from the newest match and from an event the hunt passed over, cut down to the fields the detection reads. The draft opens in the rule editor, where you review it, adjust it and save. Like every new rule, it starts in shadow. A rule made from a saved hunt stays linked to it, and deleting the hunt leaves the rule running.
A hunt with no filter can’t become a rule, since it would fire on every event.
Hunting from a finding or an incident
On a TillTell finding, owners and admins can hunt this actor, this address, this session or this target, whichever the finding carries. The actors, addresses, sessions and targets of an incident link to a hunt the same way. Each opens TillHunt on an exact search from a day before to a day after, so you see what else happened around the moment that fired. Drill findings open with drills included.
Who can hunt, and the record it leaves
Searching the stream reads every product’s security events, so TillHunt is for owners and admins only. Every run lands in the workspace audit log with its query, range and counts, including the run behind a rule draft. Saving, changing and deleting a hunt are recorded too.
From the terminal
# what the stream holds, then the question
tilldev shield hunt overview
tilldev shield hunt run action:auth.login_failed --hours 168 --group-by actor.ip --rare
# YAML from a file, over a fixed window; then continue past where it stopped
tilldev shield hunt run --yaml hunt.yaml --from 2026-09-01T00:00:00Z --to 2026-09-08T00:00:00Z
tilldev shield hunt run --yaml hunt.yaml --from 2026-09-01T00:00:00Z --to 2026-09-08T00:00:00Z --before 2026-09-05T11:02:41.118Z,<event-id>
# compile without reading events; exits 1 on problems
tilldev shield hunt check 'actor.user_agent:*curl*'
# save it, run it as saved, turn it into a rule in shadow
tilldev shield hunt save "Failed sign-ins by address" action:auth.login_failed --group-by actor.ip --hours 168
tilldev shield hunt run --saved <hunt-id>
tilldev shield hunt rule "Password guessing from one address" --saved <hunt-id> --level high --createThe same operations are on the HTTP API under /api/shield/hunts, documented in the API reference.