Your first practice run
So you want to practice — perfect, you’re on the right page. Below is a pretend interview: a task, a candidate writing code against it, and ten spare minutes of yours. Open Interview Cop next to this browser window and play along for real — the app can’t tell a practice screen from a live one.
Right now your app should look like this: only Start is lit up, and the hint square at the top already says what to aim at first.
Start by capturing the technical challenge — what the candidate is asked to solve.
Your app right now: Start is ready, the rest wakes up later.
Capture the technical challenge
Start takes a screenshot: press it, and your cursor turns into a crosshair for selecting any part of your screen.
Every session begins there — with the task itself, before a single line of code exists. The ticket, the doc, the message you sent: whatever the candidate is working from. Here’s the one our pretend candidate got:
1# The challenge — as sent to the candidate23Build a rate limiter for our API.45Rules:6- at most `limit` requests per user7- within a rolling window of `window_seconds`8- allow?(user_id) answers true or false910Keep memory sane when users go quiet.
Try it now: in your app, press Start, then drag over the challenge above. One screenshot is enough here — if a real task were longer, you’d add the rest in a moment.
What just unlocked
The moment your first capture lands, the app itself tells you what to do — the hint square changes to:
Keep capturing — add more challenge details or candidate’s work as it unfolds. Use Next when ready
And two more buttons wake up: Next, and Run Cop wearing a small 1.
The 1 counts captures waiting to be analyzed — so far, just the challenge.
You could press Run Cop this second, and nothing bad would happen. But look at what it’s holding: one screenshot, the task, and nothing else. You’d get questions about the challenge — not about the person sitting in front of you. That’s the other half, and it’s the half you came for.
So keep capturing. Start belongs at the top of an interview, pressed exactly once. Next adds every screenshot after it to the same session, so the whole story stays in one place — what you asked for, and what the candidate has written so far.
Capture the candidate’s work
Let’s pretend the interview is 10 minutes in, and the candidate has written this much:
1# Task: allow at most `limit` requests per user per window2class RateLimiter3def initialize(limit, window_seconds)4@limit = limit5@window = window_seconds6@requests = Hash.new { |h, k| h[k] = [] } # user_id => [timestamps]7end89def allow?(user_id)10now = Time.now.to_f11recent = @requests[user_id].select { |t| now - t < @window }12return false if recent.size >= @limit1314recent << now15@requests[user_id] = recent16true17end18end
Try it now: press Next — not Start — and drag over the candidate’s code above. The whole window or just a slice: checking everything or one suspicious part is your call.
Run Cop now reads 2 — two captures waiting, the task and the code:
Two screenshots, one session. Now there's something to compare.
And the hint square moves on as well:
Use Run Cop to get AI questions based on current captures. You can keep adding more anytime.
Run Cop, then the arrow
Run Cop sends everything you’ve captured off for analysis. For a short while the button locks and fills with moving stripes:
Thinking. It usually takes a few seconds.
When the stripes are gone, the arrow button underneath starts glowing — and the hint says the same thing:
Your results are ready — click the arrow button below to open the panel.
The arrow opens the results panel.
Here are the questions you can ask the candidate:
- 1
The task asks you to keep memory sane when users go quiet. Where does that happen in your code?
Listen for: nowhere yet — @requests keeps an entry for every user forever; a cleanup pass, or dropping empty lists on read, would do it
- 2
Why
Hash.newwith a block and not just{}? What does the block do?Listen for: the block inserts an empty array when you read a missing user — so even a plain read grows the hash
- 3
Two requests from the same user land at the same moment. Can both get through?
Listen for: yes — both can read the same list before either writes back; the fix is a Mutex around check-then-add
Three button presses — Start, Next, Run Cop — and you’re already holding questions about decisions the author actually made, weighed against what you actually asked for. Isn’t that cool?
Noticed the window fading in and out? That’s the More Transparent / More Opaque header — slide your mouse along it and the whole results window changes opacity in real time. It’s deliberate, and it’s the feature you’ll love on a laptop.
Make the window translucent and you can read your questions and watch the candidate’s screen through them — at the same time, without ever closing or reopening anything. One mouse move to check the code, another to read the next question, while the candidate keeps talking.
The candidate kept typing
Back in your app, notice that Run Cop is locked again — grey, and with no number on it:
Counter back to zero: nothing new since the last run, nothing new to analyze.
The hint says it in the app’s own words:
Nice work — use Next to grab more screens, then hit Run Cop for updates
To generate new questions, it needs new context. Luckily, our pretend candidate didn’t sit still. Here’s their screen at the 20-minute mark:
1# Task: allow at most `limit` requests per user per window2class RateLimiter3def initialize(limit, window_seconds)4@limit = limit5@window = window_seconds6@requests = Hash.new { |h, k| h[k] = [] } # user_id => [timestamps]7Thread.new { loop { sleep(@window); cleanup! } }8end910def allow?(user_id)11now = Time.now.to_f12recent = @requests[user_id].select { |t| now - t < @window }13return false if recent.size >= @limit1415recent << now16@requests[user_id] = recent17true18end1920def remaining(user_id)21now = Time.now.to_f22recent = @requests[user_id].select { |t| now - t < @window }23[@limit - recent.size, 0].max24end2526def cleanup!27now = Time.now.to_f28@requests.each_value { |ts| ts.reject! { |t| now - t >= @window } }29@requests.delete_if { |_, ts| ts.empty? }30end31end
Try it now: press Next again and select the code above. Don’t hunt for just the new lines: simply select the current version of whatever you want to ask about.
The capture lands, and Run Cop wakes up with a 1 — one new screenshot since the last run:
The challenge and the first draft are still in the session — the 1 is just what's new.
Fresh context, ready to go. Press it. Same as before: stripes for a few seconds, then the arrow. This time the familiar window shows run #2 — follow-up questions that build on everything you’ve captured so far:
New questions about the new code:
- 1
This thread you start in the constructor — when does it stop?
Listen for: it never stops — every new RateLimiter leaks a thread; a stop method or a daemon thread would fix it
- 2
You clean old timestamps in two places now —
allow?andcleanup!. Why two places?Listen for: allow? keeps the answers correct; cleanup! frees memory for users who went quiet — they do different jobs
- 3
allow?andremainingshare the same three lines. Would you leave it like that?Listen for: pull those lines into one small helper method and call it from both places
That’s the whole loop
From here to the end of a real interview it’s the same three moves, over and over: Next → Run Cop → arrow. Capture when the code changes, run when you want questions, read them straight off the glass.
Hope that wasn’t too hard. It isn’t — that’s the point.