Dogfood Skill: Systematic Web App QA Testing in Hermes Agent
Exploratory QA of web apps: find bugs, evidence, reports.
Written by Neura Market from the official Hermes Agent documentation for Dogfood. Commands, paths, and version numbers are reproduced from the source unchanged.
Read the official documentationThe Dogfood skill turns Hermes Agent into an exploratory QA tester for web applications. It drives a real browser through browser_exec, walks the site the way a curious user would, captures evidence when something breaks, and writes a structured bug report at the end. Reach for it when you need a first pass over a web app before a release, when you want reproducible evidence for a bug you suspect, or when nobody has time to click through every form and navigation path by hand.
What it does
The skill gives the agent a five-phase procedure: plan the scope, explore the app, collect evidence, categorise findings, and produce a report. It is not a unit-test runner and it does not replace a scripted regression suite. It is closer to a disciplined manual tester who happens to be an agent: it navigates pages, reads the DOM, clicks and types, watches the JavaScript console, takes screenshots, and records what went wrong in a format a developer can act on.
The output is a directory containing screenshots and a report.md with an executive summary, per-issue sections, a summary table, and testing notes. Issues are classified by severity (Critical, High, Medium, Low) and category (Functional, Visual, Accessibility, Console, UX, Content).
Before you start
Two things are required before the skill can do anything useful.
First, the browser toolset must be enabled so that browser_exec is available. That helper runs Python with a set of pre-imported browser functions: new_tab, goto_url, wait_for_load, page_info, js, fill_input, click_at_xy, capture_screenshot, and cdp. Browser state persists between calls, but Python variables do not, so anything you need later has to be printed or stored in the page itself.
Second, you need a target URL and a testing scope from the user. The scope can be a specific feature area or "full site" for comprehensive coverage. An optional output directory can be supplied; if it is omitted, the default is ./dogfood-output.
The skill is bundled with Hermes Agent, version 1.0.0, authored by Teknium (teknium1), and released under the MIT licence. It runs on linux, macos, and windows.
The five-phase workflow
Phase 1: Plan
Start by creating the output directory structure:
{output_dir}/
├── screenshots/ # Evidence screenshots
└── report.md # Final report (generated in Phase 5)
Then identify the testing scope from the user's input and sketch a rough sitemap. The skill suggests covering the landing or home page, navigation links in the header, footer, and sidebar, key user flows such as sign up, login, search, and checkout, forms and interactive elements, and edge cases like empty states, error pages, and 404s. This plan is not a contract; it is a checklist that keeps the exploration from drifting.
Phase 2: Explore
This is where most of the work happens. The skill advises batching each step's actions into a single browser_exec call: navigate, wait, extract, act, and check errors in one round trip. Fewer calls mean fewer chances for the hook to fall out of place.
Navigate with a console hook installed
Register the hook on the tab before navigating so that errors thrown during page load are captured too:
# Open page with console-error capture
HOOK = """window.__qa_errors = [];
const _e = console.error;
console.error = (...a) => { window.__qa_errors.push(a.map(String).join(' ')); _e(...a); };
addEventListener('error', e => window.__qa_errors.push('uncaught: ' + e.message));
addEventListener('unhandledrejection', e => window.__qa_errors.push('rejection: ' + String(e.reason)));"""
new_tab('about:blank') # first call only: the hook binds to the current tab
cdp('Page.addScriptToEvaluateOnNewDocument', source=HOOK)
goto_url("https://example.com/page")
wait_for_load()
print(page_info())
print(js('window.__qa_errors || []'))
The hook collects console.error calls, uncaught exceptions, and unhandled promise rejections into window.__qa_errors. Because it is installed through Page.addScriptToEvaluateOnNewDocument, it survives navigation. Re-register it at the start of any later call that navigates. Silent JavaScript errors are described in the source as high-value findings, and this is the mechanism that surfaces them.
Read the page structure
List the visible interactive elements and their viewport centres. When you need roles and names, use cdp('Accessibility.getFullAXTree') and filter the result in Python:
# List interactive elements
print(js("""(() => [...document.querySelectorAll('a,button,input,select,textarea,[role=button]')]
.filter(e => e.offsetParent).slice(0, 80).map((e, i) => { const r = e.getBoundingClientRect();
return {i, tag: e.tagName, text: (e.innerText || e.value || e.getAttribute('aria-label') || '').trim().slice(0, 60),
id: e.id, name: e.name, x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2)}; }))()"""))
This gives you a compact inventory of what can be clicked or typed into, with coordinates that click_at_xy can use later.
Visually assess the page
Call capture_screenshot() and inspect the attached image for layout problems, broken elements, or accessibility concerns. If your model cannot view images, pass the printed path to vision_analyze instead.
Test interactive elements systematically
The skill lists the moves to make: click buttons and links with js("document.querySelector('#submit').click()") or click_at_xy(x, y) using a centre from the element listing; fill forms with fill_input('input[name=email]', 'test input'); test keyboard navigation with cdp('Input.dispatchKeyEvent', type='keyDown', key='Tab', code='Tab', windowsVirtualKeyCode=9) followed by the matching keyUp, then js('document.activeElement.outerHTML.slice(0, 200)') to see where focus landed, and the same pattern with key='Enter', code='Enter', windowsVirtualKeyCode=13; scroll through content with js('window.scrollBy(0, 800)'); go back with js('history.back()') followed by wait_for_load(); test form validation with invalid inputs; and test empty submissions.
Check after every interaction
In the same call, look for console errors with print(js('window.__qa_errors || []')), take a capture_screenshot() and compare it with the previous one, and compare expected against actual behaviour using page_info() or a targeted js() read of the relevant element.
Phase 3: Collect evidence
For every issue found, take a screenshot with capture_screenshot(), which prints the saved path. Copy that file into {output_dir}/screenshots/ under a descriptive name such as issue-03-broken-login.png, because the report will reference it.
Record the URL where the issue occurs, the steps to reproduce, the expected behaviour, the actual behaviour, any console errors, and the screenshot path. Then classify the issue using the taxonomy in references/issue-taxonomy.md: severity is Critical, High, Medium, or Low, and category is Functional, Visual, Accessibility, Console, UX, or Content.
Phase 4: Categorise
Review everything collected, merge duplicates that are the same bug showing up in different places, assign final severity and category, sort by severity with Critical first, and count issues by severity and category for the executive summary.
Phase 5: Report
Generate the final report using the template at templates/dogfood-report-template.md. It must include an executive summary with total issue count, breakdown by severity, and testing scope; per-issue sections with issue number and title, severity and category badges, the URL where observed, a description, steps to reproduce, expected versus actual behaviour, screenshot references using MEDIA: for inline images, and console errors where relevant; a summary table of all issues; and testing notes covering what was tested, what was not, and any blockers. Save the report to {output_dir}/report.md.
Tools reference
All browser actions are browser_exec calls. The source summarises the idioms in a table:
| Action | browser_exec idiom |
|---|---|
| Go to a URL | new_tab(url) once (or new_tab('about:blank') before registering the hook), then goto_url(url); follow with wait_for_load() |
| Page state | page_info() |
| DOM / accessibility tree | js(...) extraction, or cdp('Accessibility.getFullAXTree') filtered in Python |
| Click | js("document.querySelector(sel).click()") or click_at_xy(x, y) |
| Type into an input | fill_input(selector, text) |
| Scroll | js('window.scrollBy(0, 800)') |
| Go back | js('history.back()') |
| Press a key | cdp('Input.dispatchKeyEvent', type='keyDown', key=..., code=..., windowsVirtualKeyCode=...) + keyUp |
| Screenshot | capture_screenshot() (image attached to the result; text-only models use vision_analyze on the path) |
| JS console errors | hook via cdp('Page.addScriptToEvaluateOnNewDocument', source=HOOK), read with `js('window.__qa_errors |
Tips from the source
Read window.__qa_errors after navigating and after significant interactions. Batch each sub-procedure into one browser_exec call so the hook stays in place and round trips stay low. Take a capture_screenshot() when you need to reason about element positions or when DOM extraction is ambiguous; click_at_xy takes the centres from the element listing. Test with both valid and invalid inputs, because form validation bugs are common. Scroll through long pages, since content below the fold may have rendering issues. Test navigation flows end to end. Check responsive behaviour by noting layout issues visible in screenshots. Do not forget edge cases: empty states, very long text, special characters, and rapid clicking. When reporting screenshots to the user, include MEDIA: so they can see the evidence inline.
When not to use it
The source does not name explicit alternatives or exclusions. It is an exploratory QA procedure, so it fits situations where you want a human-like pass over a running web app and a written record of what broke. It is not described as a substitute for automated regression tests or for any other specific tool.
Limits and gotchas
The biggest practical constraint is the split between browser state and Python state. Browser state persists across browser_exec calls, but Python variables do not, so the console hook has to be re-registered at the start of any later call that navigates. If you forget, errors thrown during that navigation will not be captured.
The hook binds to the current tab, which is why the first call uses new_tab('about:blank') before installing it. The skill also notes that capture_screenshot() attaches the image to the result, and text-only models must route the printed path through vision_analyze instead of looking at it directly.
Finally, the report is only as good as the evidence collected. Screenshots must be copied into {output_dir}/screenshots/ under descriptive names, and every issue needs a URL, reproduction steps, expected and actual behaviour, and any console errors. The source treats silent JavaScript errors as among the most valuable findings, so skipping the console check after an interaction is the easiest way to miss the point of the exercise.
Related
The source references two supporting files inside the skill: references/issue-taxonomy.md for severity and category definitions, and templates/dogfood-report-template.md for the report structure. Both are part of the skill's own directory and are used in Phases 3 and 5 respectively.