Drive Amazon Quick on macOS programmatically — desktop app and web, one protocol (CDP)
English · 简体中文
If you can click it in the Amazon Quick UI, you can automate it: send messages, read replies, generate dashboards, build apps, build flows, run deep research, manage spaces and dashboards.
Warning
Sample code — not for production. This repository is AWS sample code for demonstration and learning. It is provided as is, with no SLA and no backwards-compatibility promise. It drives the real, already-signed-in application on your own machine over the Chrome DevTools Protocol, which means:
- Automated actions really happen (creating a space, deleting a space, publishing an app are all real). Run it against a throwaway or test account first.
- Opening a CDP debug port means any local process that can reach that port can control that browser instance. This project binds to the loopback address
127.0.0.1only — do not change it to0.0.0.0and do not expose it. - UI automation depends on UI structure by nature. When the Amazon Quick front end changes, selectors may break and need re-probing.
- Before any production use, do your own security review, least-privilege scoping, and error-handling hardening.
Note
Language of the CLI output. Command names, flags, exit codes, and the JSON contracts are English and stable. The human-readable messages the scripts print to stdout/stderr are currently Simplified Chinese (for example ✓ 已切换 feed 源, ⚠️ 尚未登录). If you script against these tools, branch on exit codes and JSON fields, not on message text.
An orchestrator (Claude Code, your own Python script, a CI job) remote-controls two target processes. The protocol is Chrome DevTools Protocol in both cases; only the process and the port differ:
| Target | Process | Mechanism | Script |
|---|---|---|---|
| 🟢 Desktop | Amazon Quick Desktop (Electron) | CDP, debug port :9333 |
quick_ctl.py |
| 🟢 Web | Google Chrome → Amazon Quick on the web | CDP, dedicated persistent profile ~/.chrome-cdp-profile, debug port :9445 |
chrome_as.py + web_ensure.py |
What each port is (don't connect to the wrong one):
| Port | What it is | Used for |
|---|---|---|
:9333 |
CDP of the Quick Electron app | Desktop channel (override with QUICK_CDP_PORT) |
:9445 |
CDP of the dedicated persistent-profile Chrome | Web channel (override with QUICK_WEB_CDP_PORT) |
| anything else | A local service that has nothing to do with DevTools | Do not connect — a LISTENing port is not proof of a DevTools endpoint; see gotcha #2 below |
The full path from nothing to a drivable web channel — including the human gate (the first login) — is in the control-flow diagram:
Why the web channel uses its own persistent profile: CDP requires --remote-debugging-port, and your everyday browser normally doesn't run with that port open — nor should you restart it just for automation. A dedicated --user-data-dir keeps the debug instance and your main browser out of each other's way, while persisting the login cookie in that directory — so you log in by hand exactly once, and every later ensure is idempotent.
- 🖥️ 59 desktop commands: chat / history / model and thinking effort / attachments / navigation / settings / Activity feed / Agents / Mission Control / connectors / per-message actions
- 🗣️ Natural-language triggers: Research deep dives, one-sentence app generation, scheduled Flows, chat agents
- 🌐 35 web commands: navigate the 11 areas, search, filter, sort, paginate, create and delete spaces, create / edit / publish / share apps, native CDP screenshots
- 📟 Callable from higher-level scripts: every script is a self-contained CLI, so
subprocessis all you need to compose them into any playbook
| Requirement | Notes |
|---|---|
| macOS | Depends on the fixed paths /Applications/Amazon Quick.app and /Applications/Google Chrome.app, and on launching local processes with a debug port |
| Amazon Quick | Desktop app installed under /Applications/; the web channel needs a Quick account you have access to |
uv |
Python package manager (install). This project always uses uv run — never bare pip / python |
| playwright | uv run --with playwright python -m playwright install chromium. Both channels attach to an already-running local instance (Electron / real Chrome), so the downloaded Chromium isn't used — but the package has to be there |
| Python ≥ 3.10 | The requires-python floor in pyproject.toml; CI runs on both 3.10 and 3.13 |
| An account you can sign in to | The first time you run web_ensure.py login, sign in by hand in the Chrome window that opens. This project does not sign in for you and never touches or stores credentials |
| Variable | Default | Purpose |
|---|---|---|
QUICK_CDP_PORT |
9333 |
CDP port of the Quick desktop app |
QUICK_WEB_CDP_PORT |
9445 |
CDP port of the web-channel Chrome |
QUICK_WEB_PROFILE |
~/.chrome-cdp-profile |
Dedicated persistent profile directory for the web channel (this is where the session lives) |
QUICK_WEB_REGION |
us-east-1 |
Region your Quick web instance is in |
QUICK_WEB_ACCOUNT |
your-quick-account (placeholder) |
Your Quick account name. No real account is baked into the code — you must set this before using the web channel |
QUICK_WEB_BASE |
composed from the two above | Override the whole start-URL prefix (setting this ignores region/account) |
QUICK_UNAUTH_MARKERS |
signin,login,authorize,oauth,sso |
Comma-separated URL markers that mean "signed out"; matching any one of them counts as signed out. Identity-provider domains differ per organization, so override as needed |
# Typical setup: point at your own account and region
export QUICK_WEB_REGION=us-east-1
export QUICK_WEB_ACCOUNT=my-quick-account
# Or override the start URL wholesale (trailing slash optional — the code normalizes it)
export QUICK_WEB_BASE=https://us-east-1.quicksight.aws.amazon.com/sn/account/my-quick-account/start
# If your sign-in redirect domain isn't covered by the defaults, add your own keyword
export QUICK_UNAUTH_MARKERS=signin,login,authorize,oauth,sso,my-idpAn empty or whitespace-only
QUICK_UNAUTH_MARKERSfalls back to the default rather than silently disabling the signed-in check.
git clone https://github.com/aws-samples/sample-quick-control.git
cd sample-quick-control/scripts
# ── Desktop: make sure it runs with the debug port → ask a question → screenshot ──
uv run --with playwright python quick_ctl.py ensure
uv run --with playwright python quick_ctl.py ask "Analyze the anomalies in this revenue data"
uv run --with playwright python quick_ctl.py screenshot /tmp/q.png
# ── Web: one command brings up the persistent-profile instance ──
export QUICK_WEB_ACCOUNT=my-quick-account # set this to your own account first
uv run python web_ensure.py ensure # idempotent: returns immediately if already running
uv run python web_ensure.py login # first time: sign in by hand in the window that opens
uv run python web_ensure.py doctor # two checks: CDP port / signed-in state
uv run python web_ensure.py status # JSON: cdp_ok / signed_in / url / tabs / port / profile
# ── Drive the web channel (the session persists; restarting the instance needs no re-login) ──
uv run --with playwright python chrome_as.py goto spaces
uv run --with playwright python chrome_as.py list
uv run --with playwright python chrome_as.py search "quarterly revenue"
uv run --with playwright python chrome_as.py screenshot /tmp/web.pngweb_ensure.py has five commands: ensure (idempotently get the channel ready) / status (JSON state) / login (wait for you to sign in) / doctor (health check plus fix suggestions) / restart (restart the instance after changing the port or clearing state).
Writes have guardrails: destructive commands require explicit confirmation. For example
chrome_as.py delete-spacewithout--confirmrefuses to run and exits with code 2.
This project has a dual identity: it is a standalone set of CLI tools, and it is also a Claude Code Skill (declared through the frontmatter in SKILL.md). Installing it means symlinking the repository into ~/.claude/skills/:
# 1. Clone wherever you keep code (any path works)
git clone https://github.com/aws-samples/sample-quick-control.git ~/Code/sample-quick-control
# 2. Symlink it into the skills directory
# ⚠️ The directory name must match the `name` in SKILL.md's frontmatter (currently amazon-quick-control)
ln -s ~/Code/sample-quick-control ~/.claude/skills/amazon-quick-control
# 3. Verify
ls -la ~/.claude/skills/amazon-quick-control- Why a symlink instead of a copy: edit the code in the repo and the skill picks it up immediately — no reinstall, no two copies to keep in sync.
- When changes take effect: the body of
SKILL.mdis read on every invocation, so edits apply instantly. But thename/descriptionin the frontmatter are loaded at startup — change a trigger phrase and you need to restart Claude Code. - Just want the plain CLI? Skip step 2. The
scripts/*.pyfiles are self-contained; run them withuv rundirectly. - Note that
SKILL.mditself is written in Chinese, since its trigger phrases are Chinese.
| Script | What it does |
|---|---|
quick_ctl.py |
Desktop CDP control, 59 commands (chat / history / model / attachments / navigation / settings / message actions / wait strategies), port :9333 |
chrome_as.py |
Web CDP control, 35 commands (navigate / perceive / click / fill / read lists / screenshot / create and delete spaces / create and publish apps), port :9445 |
web_ensure.py |
Bring-up and self-healing for the web channel: ensure / status / login / doctor / restart |
map_screen.py, crawl_all.py |
UI element harvesting (one screen / a walk across the desktop screens), dumped to /tmp/quickmap/*.json |
crawl_web_deep.py, probe_web_areas.py |
Harvesting and probing of the 11 web areas — |
probe_settings.py |
Probes the desktop settings screens |
verify_all.py |
Exercises every desktop capability in a single session and prints a PASS/FAIL report |
uv run --with pytest --with playwright --with 'ruff>=0.12,<0.13' python -m pytest tests/ -vThree layers, 84 tests in total. What you see depends on what's running locally: with the Quick desktop app up, this machine reports 79 passed / 5 skipped (the 5 skips need a signed-in web channel); with no local Quick at all, 76 passed / 8 skipped.
- Unit (
test_unit.py, 21 tests): the 11-area alias mapping; completeness of the_VIEW_MAP/_FILTER_MAP/_PAGE_MAPtables;WEB_BASEURL composition andQUICK_WEB_ACCOUNTresolution; CDP endpoints being loopback-only; thestatusJSON contract; the persistent profile being distinct from your main profile; the signed-in test andQUICK_UNAUTH_MARKERSoverride behaviour — no real machine required - E2E (
test_e2e.py, 9 tests): 3 desktop (status/ port /read) plus 6 web (cdp_ok/signed_ininstatus, signed-in state, thegoto+listread path, native CDP screenshots, soft failure exiting 0,delete-spacewithout--confirmexiting 2) — auto-skipped when the channel isn't ready; all read-only, creates no assets - Regression (
test_regression.py, 54 tests): all 9 scripts parsed at therequires-pythonfloor (ast.parse(feature_version=)), plus aruff --isolated --target-version py310 --select E9syntax gate as a backstop, plus a contract guardrail for each of the 35 web commands (dispatch checked via AST, so it isn't pinned to one coding style), plus--confirmactually gating (AST-checked forsys.exit), plus port conventions staying consistent
Real-machine tests decide their skips by probing the channels in
tests/conftest.py. The key point: the probe must verify that the discovery layer really responds (/json/version) rather than merely checking that a port is LISTENing — a port that's open but isn't DevTools is a real situation, and port-only probing gets it wrong. Ports are read fromQUICK_WEB_CDP_PORT/QUICK_CDP_PORT, the same source the scripts under test use, so changing a port doesn't produce false reds.The signed-in test in the suite comes from the same source as
web_ensure.unauth_markers()(conftest imports it directly) instead of keeping a second copy, so the two can't drift apart.
The runner has neither the Quick desktop app nor a CDP Chrome, so pytest runs in two steps, each asserting something different:
| Step | Command gist | Expectation |
|---|---|---|
| No-real-machine layer | -m "not realmachine" |
All pass / 0 skipped — any skip here means a guardrail was silenced |
| Real-machine layer | -m realmachine |
All skipped / exit 0 — this step is the regression test for the conftest skip logic |
- The workflow hardcodes
QUICK_CDP_PORT=59333/QUICK_WEB_CDP_PORT=59445(ports nothing listens on), so a stray service occupying 9333/9445 on the runner can't "wake up" the real-machine tests and fail them. - There are two lint jobs:
ruff check .(pinned toruff>=0.12,<0.13, the same range aspyproject.toml, to avoid rule drift) and theruff --isolated --target-version py310 --select E9syntax gate (--isolatedbypasses the pyproject ignores, so tuning that ignore list can't get around it). - The pytest matrix covers 3.10 and 3.13: 3.10 is the floor declared by
requires-python, and incompatibilities like a backslash inside an f-string only surface on 3.10. ⚠️ The pytest step must include--with 'ruff>=0.12,<0.13'—test_min_python_syntax_gateusesshutil.which("ruff")in its skipif, so without ruff onPATHit skips silently and CI goes falsely green.- There is also a hygiene job: a scan for credential-shaped strings, plus a consistency check between the version in
pyproject.tomland the top version inCHANGELOG.md.
Expand 8 field notes
- Debug ports only open at launch: a running Chrome / Electron cannot have a debug port attached hot — it must be restarted with
--remote-debugging-port.quick_ctl.py ensurehandles that for the desktop; the web side launches with a persistent profile plus--remote-debugging-port=9445. - A LISTENing port is not a DevTools endpoint: some local service listening on a port does not make it DevTools. We once read "the process wasn't launched with debug flags + an unrelated service holds the port" as "corporate policy blocks DevTools", and on the strength of that misreading locked the entire web channel into a convoluted workaround for nearly a release cycle. Correct order of investigation: confirm the target process's launch flags first, and only then talk about external blocking. Use
GET /json/versionreturning browser info as the test — notlsof. - Single-session discipline: on a real machine there is one browser instance and one session. Do not drive it concurrently — prefer reads, one action at a time. If you want parallelism, parallelize "analyzing data you already dumped".
- The i18n trap in delete confirmations: the delete-space dialog asks you to type
Delete, but what it actually validates is the localized word (in a Chinese UI, 「删除」). Confirmation words for deletions must match the UI language — don't copy the English prompt verbatim. - Artifact generation has long silent stretches: while Quick builds a dashboard and runs code, the UI emits no text, so a "text has stabilized" heuristic declares completion too early. Use
wait-done(watch for the Stop button to disappear) plus a fixed buffer. Reliable >> fast. - SPA timing: text appearing ≠ the element being interactive (table rows and buttons mount later). Every click and row lookup uses self-retrying polling, not a fixed
sleep. - Screen-recording permission: without the "Screen Recording" permission, a terminal's
screencapturefails withcould not create image from display. CDP is more direct:page.screenshot()doesn't need Screen Recording permission at all. - Three traps with playwright over CDP: don't call
b.new_page()(it raisesBrowser context management is not supported); reuse the page insideb.contexts[0]whose URL matches the target site; open new tabs through the CDP HTTP endpointPUT /json/new?<url>.
Historical constraints from the early AppleScript implementation — gone since the move to CDP (kept for the record):
- Never hand-assemble JS inside a shell heredoc: shell / AppleScript / JS made three layers of escaping hell. Arrow functions
=>, spread...,<,\s, or a curly quote misplaced in any one layer produced asyntax error; back then all JS was forced down to ES5. → Over CDP,page.evaluatetakes a function object directly. There is no escaping layer, modern JS works as measured, and the ES5 constraint is void. - Rich front-end components resisted automation → the clipboard hack: the confirmation box is a controlled input component, and pure JS injection (
nativeSetter/_valueTracker/ synthetic event chains) was entirely ineffective. The only working answer back then waspbcopy+ bringing the tab to the foreground +Cmd+A/Cmd+V. → Over CDP, playwright's nativelocator.fill()writes straight in, as measured. The clipboard hack and the must-be-foregrounded requirement both retired together.
This project creates no cloud resources; cleanup is entirely local:
# 1. Kill the debug Chrome instance (only the one on this dedicated profile — your main browser is untouched)
pkill -f "user-data-dir=$HOME/.chrome-cdp-profile"
# 2. Delete the persistent profile directory (it holds the login cookie — you'll sign in again next time)
rm -rf ~/.chrome-cdp-profile
# 3. Dumps produced by the harvesting scripts
rm -rf /tmp/quickmap
# 4. If you installed it as a Claude Code Skill, remove the symlink (this does not delete your clone)
rm ~/.claude/skills/amazon-quick-control
# 5. Delete the clone itself
rm -rf ~/Code/sample-quick-controlThe Quick desktop app is your own application — quit it however you normally would. This project changes none of its persisted settings, except when you explicitly call a
set-*command.
- Never touches credentials: this project does not read, store, or transmit any username / password / token / cookie. Signing in is done entirely by you, by hand, in the browser window that opens; the session is persisted by that profile directory itself.
- Loopback only: every CDP connection goes to
127.0.0.1, guarded by a unit test (test_cdp_endpoint_uses_loopback). Do not bind the debug port to0.0.0.0or forward it to the internet — that hands over control of the browser. - The persistent profile directory holds a live session:
~/.chrome-cdp-profilecontains session cookies. Treat it as sensitive data (don't back it up to a public repo, don't hand it to anyone else), and delete it via the Cleanup section when you're done. - Destructive operations require explicit confirmation: delete commands must carry
--confirmor they refuse to run. - Don't put secrets in
QUICK_WEB_*: these variables are only a region / account name / URL prefix, and they show up in process listings and logs. - Report security issues as described in SECURITY.md (please do not open a public issue).
Contributions are welcome — please read CONTRIBUTING.md first. Participation in this project is governed by CODE_OF_CONDUCT.md.
This project is licensed under MIT-0 — see LICENSE. Release history is in the CHANGELOG.