claude-human 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. claude_human-0.2.0/LICENSE +21 -0
  2. claude_human-0.2.0/PKG-INFO +195 -0
  3. claude_human-0.2.0/README.md +175 -0
  4. claude_human-0.2.0/pyproject.toml +31 -0
  5. claude_human-0.2.0/setup.cfg +4 -0
  6. claude_human-0.2.0/src/claude_human/__init__.py +4 -0
  7. claude_human-0.2.0/src/claude_human/__main__.py +5 -0
  8. claude_human-0.2.0/src/claude_human/cli.py +222 -0
  9. claude_human-0.2.0/src/claude_human/paths.py +56 -0
  10. claude_human-0.2.0/src/claude_human/screenshot.py +318 -0
  11. claude_human-0.2.0/src/claude_human/skill/SKILL.md +222 -0
  12. claude_human-0.2.0/src/claude_human/station/__init__.py +31 -0
  13. claude_human-0.2.0/src/claude_human/station/auth.py +131 -0
  14. claude_human-0.2.0/src/claude_human/station/input.py +189 -0
  15. claude_human-0.2.0/src/claude_human/station/prepare.py +343 -0
  16. claude_human-0.2.0/src/claude_human/station/server.py +299 -0
  17. claude_human-0.2.0/src/claude_human/station/view.py +510 -0
  18. claude_human-0.2.0/src/claude_human/tools/sckshot/build.sh +78 -0
  19. claude_human-0.2.0/src/claude_human/tools/sckshot/sckshot.swift +100 -0
  20. claude_human-0.2.0/src/claude_human/tools/vhid/build.sh +34 -0
  21. claude_human-0.2.0/src/claude_human/tools/vhid/type_string.cpp +132 -0
  22. claude_human-0.2.0/src/claude_human/unlock.py +217 -0
  23. claude_human-0.2.0/src/claude_human.egg-info/PKG-INFO +195 -0
  24. claude_human-0.2.0/src/claude_human.egg-info/SOURCES.txt +34 -0
  25. claude_human-0.2.0/src/claude_human.egg-info/dependency_links.txt +1 -0
  26. claude_human-0.2.0/src/claude_human.egg-info/entry_points.txt +2 -0
  27. claude_human-0.2.0/src/claude_human.egg-info/requires.txt +8 -0
  28. claude_human-0.2.0/src/claude_human.egg-info/top_level.txt +1 -0
  29. claude_human-0.2.0/tests/test_build_scripts.py +47 -0
  30. claude_human-0.2.0/tests/test_cli.py +112 -0
  31. claude_human-0.2.0/tests/test_paths.py +47 -0
  32. claude_human-0.2.0/tests/test_screenshot.py +72 -0
  33. claude_human-0.2.0/tests/test_station.py +331 -0
  34. claude_human-0.2.0/tests/test_station_input.py +140 -0
  35. claude_human-0.2.0/tests/test_station_prepare.py +126 -0
  36. claude_human-0.2.0/tests/test_unlock.py +158 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Konstantinos Georgiou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-human
3
+ Version: 0.2.0
4
+ Summary: Screen capture, lock screen typing and a phone view of one window, for a Mac that a person and an agent share
5
+ Author: Konstantinos Georgiou
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/drkostas/claude-human
8
+ Keywords: macos,screencapturekit,screenshot,karabiner,virtual-hid,claude,remote
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: MacOS
11
+ Classifier: Topic :: Utilities
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: macos
16
+ Requires-Dist: pyobjc-framework-Quartz>=10; sys_platform == "darwin" and extra == "macos"
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest>=8; extra == "test"
19
+ Dynamic: license-file
20
+
21
+ # claude-human
22
+
23
+ claude-human is a small set of tools for a Mac that a person and an agent share. I run a system on my own Mac that hands tasks to me on my phone when it needs a person (approve a password prompt, look at a window, type a password at the lock screen). These are the parts of it that are not tied to that system.
24
+
25
+ There are four pieces.
26
+
27
+ - `claude_human.screenshot` captures the screen or one window through ScreenCaptureKit, lists the windows a person works in, and tells a locked screen apart from a password panel.
28
+ - `claude_human.unlock` types a password at the lock screen or into a SecurityAgent password panel through Karabiner's virtual HID keyboard, and then checks that it worked.
29
+ - `claude_human.station` shows one window of the Mac on a phone and turns the person's taps and keys into clicks and key presses on the Mac, so a person can do a step on the Mac from anywhere.
30
+ - `@drkostas/expo-ntfy` (in `js/`) gets messages from a self-hosted ntfy server to an Android phone without Firebase, through a native foreground service that an Expo config plugin adds to the app.
31
+
32
+ The Python parts need macOS. The JavaScript part needs an Expo app.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install 'claude-human[macos]'
38
+ claude-human build-tools
39
+ ```
40
+
41
+ `build-tools` compiles the two helpers into `~/.local/share/claude-human/bin` (change it with `--out` or `CLAUDE_HUMAN_BIN_DIR`). It needs the Xcode command line tools and git. The lock screen helper also needs [Karabiner-Elements](https://karabiner-elements.pqrs.org/) installed, because it talks to Karabiner's virtual HID daemon.
42
+
43
+ ```bash
44
+ claude-human build-tools --only sckshot --bundle-id org.example.sckshot --identity "Apple Development: Your Name (TEAMID)"
45
+ claude-human build-tools --only vhid --pqrs-commit <commit>
46
+ ```
47
+
48
+ The signing identity defaults to the first "Apple Development" identity in your keychain, and to an ad-hoc signature when there is none. With an ad-hoc signature the Screen Recording grant does not survive a rebuild, because macOS ties it to the signature. Keep the bundle id the same between builds for the same reason. `--identity none` skips signing (CI uses it).
49
+
50
+ ## Screenshots
51
+
52
+ ```bash
53
+ claude-human windows # the ordinary windows on screen, as JSON
54
+ claude-human screenshot --out screen.png # the main display
55
+ claude-human screenshot --out safari.jpg --app Safari --max-width 1200
56
+ claude-human state # locked? password panel up? a system dialog holding focus?
57
+ ```
58
+
59
+ ```python
60
+ from claude_human import screenshot
61
+
62
+ wid = screenshot.resolve("Safari", None) # look the window up again on every request
63
+ jpeg, width, height = screenshot.frame(wid, max_width=1100, quality=0.55)
64
+ ```
65
+
66
+ A few things I learned while building this.
67
+
68
+ - On recent macOS the older capture call (CGWindowListCreateImage) still works but is throttled to about one frame every 30 seconds. ScreenCaptureKit is not throttled, but its async completion never fires through PyObjC, so `sckshot` is a small Swift tool that the Python side runs. The old call is kept as a fallback.
69
+ - A window id changes every time its application restarts. Store the application name and look the id up again (`resolve`), or a stored id can reach a different window.
70
+ - When one window is asked for and the fallback cannot find its rectangle (for example, it is on another desktop), `capture` returns nothing. It never returns the whole screen in its place, because that would show every other window to someone who was given one.
71
+ - `frame` returns nothing at once while the session is locked. macOS refuses the capture then, and the fallback would block for 30 seconds and return black.
72
+ - A SecurityAgent password panel sets the same "locked" bit as the lock screen, while the desktop behind it is live. `auth_prompt()` finds the panel (a small SecurityAgent window, between the size of its helper windows and the size of the lock screen), and `frame` still captures when one is up.
73
+ - Without the Screen Recording grant, macOS lists windows with empty titles. Empty titles are the sign that the grant is missing.
74
+
75
+ ## Handing a window to a person
76
+
77
+ The station is a small web server. It streams one window (or the whole display) as MJPEG, and its page turns the phone into a trackpad for that window. One finger moves a pointer drawn on the page, a tap clicks, a long press is a right click, two fingers scroll or pinch to zoom, and a double tap held down drags. There are buttons for the keyboard, Esc, Return, Tab and Spotlight.
78
+
79
+ ```bash
80
+ claude-human station # http://127.0.0.1:8789/view, token in ~/.config/claude-human/station-token
81
+ claude-human station --app "System Settings" # the token can reach this app's window and nothing else
82
+ claude-human prepare "System Settings" "open x-apple.systempreferences:com.apple.LoginItems-Settings.extension" "wait 1"
83
+ ```
84
+
85
+ The station listens on 127.0.0.1 only, and it refuses any other address. Every click it accepts is a real click on the Mac, so reach it through something that adds its own login and encryption in front of it (a private network proxy such as `tailscale serve`, or an SSH tunnel), never directly.
86
+
87
+ The token travels only in the `Authorization: Bearer` header. The page at `/view` carries no secret. An app that embeds it hands the token in through `window.__stationGrant(token)` or a `postMessage` of `{type: "station:grant", grant}`, and a page opened in a plain browser asks for it in a password field. A token in a URL is ignored, because URLs end in histories, logs and share sheets.
88
+
89
+ ```python
90
+ from claude_human.station import Grant, StationAuth, Unavailable, make_server
91
+
92
+ class MyAuth(StationAuth):
93
+ def check(self, secret): # who is this token, right now
94
+ task = my_store.task_for_token(secret)
95
+ return Grant(master=False, station=task.window, holder=task.person) if task else None
96
+
97
+ def app_for(self, grant): # the app this grant may drive (None is the whole display)
98
+ try:
99
+ return my_store.app_of(grant.station)
100
+ except my_store.Down as e:
101
+ raise Unavailable(str(e)) # refused, never widened to the whole display
102
+
103
+ def perform(self, grant, act, run): # a Spotlight press, to record who did it
104
+ return run()
105
+
106
+ def on_input(self, grant, msg, result): # every tap and key, after it was handled
107
+ pass
108
+
109
+ make_server(MyAuth(), port=8789).serve_forever()
110
+ ```
111
+
112
+ How it keeps a person to the window they were given.
113
+
114
+ - `check` is asked on every request, so a grant you revoke stops working at once.
115
+ - A pinned grant (`master=False`) streams and drives the window of the app that `app_for` names, whatever the request asks for. It cannot list windows or choose a window id.
116
+ - When that app is not on the visible desktop, the picture ends and taps are refused. Neither falls back to the whole display, because the same point on the display is whatever the person at the Mac is looking at.
117
+ - When `app_for` cannot answer, it raises `Unavailable` and the request is refused. Returning None there would turn a passing database error into access to the whole screen.
118
+ - Taps are refused while the Mac is locked and while a system dialog holds focus, and the page says why. macOS drops synthetic input in both cases while it still reports success.
119
+ - The reason for a refusal goes to the server's log, never to the caller, and the token is never logged.
120
+ - Text is typed by pasting it and pressing Command+V, because synthetic characters do not reach SwiftUI text fields. The clipboard is put back afterwards.
121
+
122
+ `prepare` runs a recipe before the person arrives (`open`, `wait`, `search`, `type`, `key`, `click`, `scroll`, `activate`), so a shared window shows the item this task is about. `--phase place` runs only `open` and `wait`, which are safe on a desktop nobody is looking at. `--phase input` runs the rest once the person is looking at the window, because macOS sends a synthetic key to the frontmost app wherever it is. Every input step whose input the Mac refused is a failed step, and the run ends with `<app>: still open` only when the window is still there.
123
+
124
+ The station needs Screen Recording (for the picture and the window titles) and Accessibility (for the clicks) for the process that runs it.
125
+
126
+ ## Typing at the lock screen and into password panels
127
+
128
+ The lock screen and SecurityAgent panels accept input only from real hardware. Synthetic key events, synthetic clicks and Screen Sharing input are all dropped there. Karabiner's DriverKit virtual HID keyboard is seen by macOS as a physical keyboard, so `vhid_type` can type where nothing else can.
129
+
130
+ ```bash
131
+ claude-human unlock < password.txt # type at the lock screen, then wait for the lock to clear
132
+ claude-human relock # sleep the display, then wait for the lock to set
133
+ claude-human use-password # a Touch ID panel: press Return to switch to the password field
134
+ claude-human approve < password.txt # type into the password panel, then wait for it to close
135
+ ```
136
+
137
+ ```python
138
+ from claude_human import unlock
139
+
140
+ ok, detail = unlock.unlock(password) # (True, "Unlocked.")
141
+ ok, detail = unlock.approve(password, prompt_present=my_detector)
142
+ ```
143
+
144
+ How it behaves.
145
+
146
+ - The password goes to the helper on stdin. There is no command line option for it, and it never goes into argv, the environment or a log. When stdin is a terminal, the command asks for it without echo.
147
+ - Nothing is typed unless the target is there. `unlock` returns at once when the Mac is not locked, and `approve` refuses when no password panel is on screen.
148
+ - The result is checked, not assumed. `unlock` watches the lock bit clear, `approve` watches the panel close, and `relock` watches the lock bit set.
149
+ - The first keys at a lock screen are often lost because the field has not taken focus yet, so `unlock` tries a second time. It stops after two tries, because each try is a real password entry and macOS adds a delay after several wrong ones.
150
+ - The helper clears the field first (Cmd+A, Backspace) so leftover text cannot join the password.
151
+ - On the macOS password panel, Return does not press OK, and a synthetic click never reaches it. Space presses the focused button, so `approve` types the password, Tab, Tab and Space. This needs Full Keyboard Access to be on (System Settings > Keyboard), so that Tab reaches buttons.
152
+ - The helper runs under `sudo -n`, because the Karabiner daemon's socket is only open to root. Give your user a sudo rule for the helper's path, or run it as root.
153
+
154
+ Please read [SECURITY.md](SECURITY.md) before you use this part. It types a password, and it should only ever do that on your own Mac.
155
+
156
+ ## Phone notifications without Firebase
157
+
158
+ `js/` is the npm package `@drkostas/expo-ntfy`. It has a config plugin, the ntfy wire logic, and the Expo glue for notifications. See [js/README.md](js/README.md).
159
+
160
+ ## What macOS needs
161
+
162
+ - Screen Recording for `sckshot.app` (fast capture), and for the process that lists windows (window titles and the fallback capture).
163
+ - Accessibility for the process that runs the station, so that its clicks and keys reach the Mac.
164
+
165
+ - Karabiner-Elements, with its driver extension allowed, for the virtual keyboard.
166
+ - Full Keyboard Access for `approve`.
167
+
168
+ macOS judges a process started by launchd by the binary that runs, so grant these to that interpreter and not only to your terminal.
169
+
170
+ A major macOS upgrade can reset these grants. Nothing can grant them again except a person in System Settings, which is how it should be.
171
+
172
+ ## Claude Code skill
173
+
174
+ The package ships a Claude Code skill that teaches an agent how to use all of this safely. It covers building and signing the helpers, the macOS grants and what resets them, the consent rules for typing a password, the Android delivery traps, and a catalogue of the failures behind each rule.
175
+
176
+ ```bash
177
+ claude-human skill # writes ~/.claude/skills/claude-human/SKILL.md
178
+ claude-human skill --dir ./skills # another skills folder
179
+ ```
180
+
181
+ The same file is at [skill/SKILL.md](skill/SKILL.md) for anyone who uses only the npm package.
182
+
183
+ ## Development
184
+
185
+ ```bash
186
+ python -m venv .venv && .venv/bin/pip install -e '.[test,macos]'
187
+ .venv/bin/pytest
188
+ cd js && npm ci && npm test
189
+ ```
190
+
191
+ The tests do not need any grant. They cover the argument parsing, the station server (run on a free port with a fake screen and a fake input sink), its input logic and recipe runner with a stand-in for Quartz, the path settings, the window and panel detection on recorded window lists, the typing logic with a fake helper, the ntfy logic, and the config plugin run against a fixture Android project. On macOS one test also compiles `sckshot` without signing it.
192
+
193
+ ## License
194
+
195
+ MIT
@@ -0,0 +1,175 @@
1
+ # claude-human
2
+
3
+ claude-human is a small set of tools for a Mac that a person and an agent share. I run a system on my own Mac that hands tasks to me on my phone when it needs a person (approve a password prompt, look at a window, type a password at the lock screen). These are the parts of it that are not tied to that system.
4
+
5
+ There are four pieces.
6
+
7
+ - `claude_human.screenshot` captures the screen or one window through ScreenCaptureKit, lists the windows a person works in, and tells a locked screen apart from a password panel.
8
+ - `claude_human.unlock` types a password at the lock screen or into a SecurityAgent password panel through Karabiner's virtual HID keyboard, and then checks that it worked.
9
+ - `claude_human.station` shows one window of the Mac on a phone and turns the person's taps and keys into clicks and key presses on the Mac, so a person can do a step on the Mac from anywhere.
10
+ - `@drkostas/expo-ntfy` (in `js/`) gets messages from a self-hosted ntfy server to an Android phone without Firebase, through a native foreground service that an Expo config plugin adds to the app.
11
+
12
+ The Python parts need macOS. The JavaScript part needs an Expo app.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install 'claude-human[macos]'
18
+ claude-human build-tools
19
+ ```
20
+
21
+ `build-tools` compiles the two helpers into `~/.local/share/claude-human/bin` (change it with `--out` or `CLAUDE_HUMAN_BIN_DIR`). It needs the Xcode command line tools and git. The lock screen helper also needs [Karabiner-Elements](https://karabiner-elements.pqrs.org/) installed, because it talks to Karabiner's virtual HID daemon.
22
+
23
+ ```bash
24
+ claude-human build-tools --only sckshot --bundle-id org.example.sckshot --identity "Apple Development: Your Name (TEAMID)"
25
+ claude-human build-tools --only vhid --pqrs-commit <commit>
26
+ ```
27
+
28
+ The signing identity defaults to the first "Apple Development" identity in your keychain, and to an ad-hoc signature when there is none. With an ad-hoc signature the Screen Recording grant does not survive a rebuild, because macOS ties it to the signature. Keep the bundle id the same between builds for the same reason. `--identity none` skips signing (CI uses it).
29
+
30
+ ## Screenshots
31
+
32
+ ```bash
33
+ claude-human windows # the ordinary windows on screen, as JSON
34
+ claude-human screenshot --out screen.png # the main display
35
+ claude-human screenshot --out safari.jpg --app Safari --max-width 1200
36
+ claude-human state # locked? password panel up? a system dialog holding focus?
37
+ ```
38
+
39
+ ```python
40
+ from claude_human import screenshot
41
+
42
+ wid = screenshot.resolve("Safari", None) # look the window up again on every request
43
+ jpeg, width, height = screenshot.frame(wid, max_width=1100, quality=0.55)
44
+ ```
45
+
46
+ A few things I learned while building this.
47
+
48
+ - On recent macOS the older capture call (CGWindowListCreateImage) still works but is throttled to about one frame every 30 seconds. ScreenCaptureKit is not throttled, but its async completion never fires through PyObjC, so `sckshot` is a small Swift tool that the Python side runs. The old call is kept as a fallback.
49
+ - A window id changes every time its application restarts. Store the application name and look the id up again (`resolve`), or a stored id can reach a different window.
50
+ - When one window is asked for and the fallback cannot find its rectangle (for example, it is on another desktop), `capture` returns nothing. It never returns the whole screen in its place, because that would show every other window to someone who was given one.
51
+ - `frame` returns nothing at once while the session is locked. macOS refuses the capture then, and the fallback would block for 30 seconds and return black.
52
+ - A SecurityAgent password panel sets the same "locked" bit as the lock screen, while the desktop behind it is live. `auth_prompt()` finds the panel (a small SecurityAgent window, between the size of its helper windows and the size of the lock screen), and `frame` still captures when one is up.
53
+ - Without the Screen Recording grant, macOS lists windows with empty titles. Empty titles are the sign that the grant is missing.
54
+
55
+ ## Handing a window to a person
56
+
57
+ The station is a small web server. It streams one window (or the whole display) as MJPEG, and its page turns the phone into a trackpad for that window. One finger moves a pointer drawn on the page, a tap clicks, a long press is a right click, two fingers scroll or pinch to zoom, and a double tap held down drags. There are buttons for the keyboard, Esc, Return, Tab and Spotlight.
58
+
59
+ ```bash
60
+ claude-human station # http://127.0.0.1:8789/view, token in ~/.config/claude-human/station-token
61
+ claude-human station --app "System Settings" # the token can reach this app's window and nothing else
62
+ claude-human prepare "System Settings" "open x-apple.systempreferences:com.apple.LoginItems-Settings.extension" "wait 1"
63
+ ```
64
+
65
+ The station listens on 127.0.0.1 only, and it refuses any other address. Every click it accepts is a real click on the Mac, so reach it through something that adds its own login and encryption in front of it (a private network proxy such as `tailscale serve`, or an SSH tunnel), never directly.
66
+
67
+ The token travels only in the `Authorization: Bearer` header. The page at `/view` carries no secret. An app that embeds it hands the token in through `window.__stationGrant(token)` or a `postMessage` of `{type: "station:grant", grant}`, and a page opened in a plain browser asks for it in a password field. A token in a URL is ignored, because URLs end in histories, logs and share sheets.
68
+
69
+ ```python
70
+ from claude_human.station import Grant, StationAuth, Unavailable, make_server
71
+
72
+ class MyAuth(StationAuth):
73
+ def check(self, secret): # who is this token, right now
74
+ task = my_store.task_for_token(secret)
75
+ return Grant(master=False, station=task.window, holder=task.person) if task else None
76
+
77
+ def app_for(self, grant): # the app this grant may drive (None is the whole display)
78
+ try:
79
+ return my_store.app_of(grant.station)
80
+ except my_store.Down as e:
81
+ raise Unavailable(str(e)) # refused, never widened to the whole display
82
+
83
+ def perform(self, grant, act, run): # a Spotlight press, to record who did it
84
+ return run()
85
+
86
+ def on_input(self, grant, msg, result): # every tap and key, after it was handled
87
+ pass
88
+
89
+ make_server(MyAuth(), port=8789).serve_forever()
90
+ ```
91
+
92
+ How it keeps a person to the window they were given.
93
+
94
+ - `check` is asked on every request, so a grant you revoke stops working at once.
95
+ - A pinned grant (`master=False`) streams and drives the window of the app that `app_for` names, whatever the request asks for. It cannot list windows or choose a window id.
96
+ - When that app is not on the visible desktop, the picture ends and taps are refused. Neither falls back to the whole display, because the same point on the display is whatever the person at the Mac is looking at.
97
+ - When `app_for` cannot answer, it raises `Unavailable` and the request is refused. Returning None there would turn a passing database error into access to the whole screen.
98
+ - Taps are refused while the Mac is locked and while a system dialog holds focus, and the page says why. macOS drops synthetic input in both cases while it still reports success.
99
+ - The reason for a refusal goes to the server's log, never to the caller, and the token is never logged.
100
+ - Text is typed by pasting it and pressing Command+V, because synthetic characters do not reach SwiftUI text fields. The clipboard is put back afterwards.
101
+
102
+ `prepare` runs a recipe before the person arrives (`open`, `wait`, `search`, `type`, `key`, `click`, `scroll`, `activate`), so a shared window shows the item this task is about. `--phase place` runs only `open` and `wait`, which are safe on a desktop nobody is looking at. `--phase input` runs the rest once the person is looking at the window, because macOS sends a synthetic key to the frontmost app wherever it is. Every input step whose input the Mac refused is a failed step, and the run ends with `<app>: still open` only when the window is still there.
103
+
104
+ The station needs Screen Recording (for the picture and the window titles) and Accessibility (for the clicks) for the process that runs it.
105
+
106
+ ## Typing at the lock screen and into password panels
107
+
108
+ The lock screen and SecurityAgent panels accept input only from real hardware. Synthetic key events, synthetic clicks and Screen Sharing input are all dropped there. Karabiner's DriverKit virtual HID keyboard is seen by macOS as a physical keyboard, so `vhid_type` can type where nothing else can.
109
+
110
+ ```bash
111
+ claude-human unlock < password.txt # type at the lock screen, then wait for the lock to clear
112
+ claude-human relock # sleep the display, then wait for the lock to set
113
+ claude-human use-password # a Touch ID panel: press Return to switch to the password field
114
+ claude-human approve < password.txt # type into the password panel, then wait for it to close
115
+ ```
116
+
117
+ ```python
118
+ from claude_human import unlock
119
+
120
+ ok, detail = unlock.unlock(password) # (True, "Unlocked.")
121
+ ok, detail = unlock.approve(password, prompt_present=my_detector)
122
+ ```
123
+
124
+ How it behaves.
125
+
126
+ - The password goes to the helper on stdin. There is no command line option for it, and it never goes into argv, the environment or a log. When stdin is a terminal, the command asks for it without echo.
127
+ - Nothing is typed unless the target is there. `unlock` returns at once when the Mac is not locked, and `approve` refuses when no password panel is on screen.
128
+ - The result is checked, not assumed. `unlock` watches the lock bit clear, `approve` watches the panel close, and `relock` watches the lock bit set.
129
+ - The first keys at a lock screen are often lost because the field has not taken focus yet, so `unlock` tries a second time. It stops after two tries, because each try is a real password entry and macOS adds a delay after several wrong ones.
130
+ - The helper clears the field first (Cmd+A, Backspace) so leftover text cannot join the password.
131
+ - On the macOS password panel, Return does not press OK, and a synthetic click never reaches it. Space presses the focused button, so `approve` types the password, Tab, Tab and Space. This needs Full Keyboard Access to be on (System Settings > Keyboard), so that Tab reaches buttons.
132
+ - The helper runs under `sudo -n`, because the Karabiner daemon's socket is only open to root. Give your user a sudo rule for the helper's path, or run it as root.
133
+
134
+ Please read [SECURITY.md](SECURITY.md) before you use this part. It types a password, and it should only ever do that on your own Mac.
135
+
136
+ ## Phone notifications without Firebase
137
+
138
+ `js/` is the npm package `@drkostas/expo-ntfy`. It has a config plugin, the ntfy wire logic, and the Expo glue for notifications. See [js/README.md](js/README.md).
139
+
140
+ ## What macOS needs
141
+
142
+ - Screen Recording for `sckshot.app` (fast capture), and for the process that lists windows (window titles and the fallback capture).
143
+ - Accessibility for the process that runs the station, so that its clicks and keys reach the Mac.
144
+
145
+ - Karabiner-Elements, with its driver extension allowed, for the virtual keyboard.
146
+ - Full Keyboard Access for `approve`.
147
+
148
+ macOS judges a process started by launchd by the binary that runs, so grant these to that interpreter and not only to your terminal.
149
+
150
+ A major macOS upgrade can reset these grants. Nothing can grant them again except a person in System Settings, which is how it should be.
151
+
152
+ ## Claude Code skill
153
+
154
+ The package ships a Claude Code skill that teaches an agent how to use all of this safely. It covers building and signing the helpers, the macOS grants and what resets them, the consent rules for typing a password, the Android delivery traps, and a catalogue of the failures behind each rule.
155
+
156
+ ```bash
157
+ claude-human skill # writes ~/.claude/skills/claude-human/SKILL.md
158
+ claude-human skill --dir ./skills # another skills folder
159
+ ```
160
+
161
+ The same file is at [skill/SKILL.md](skill/SKILL.md) for anyone who uses only the npm package.
162
+
163
+ ## Development
164
+
165
+ ```bash
166
+ python -m venv .venv && .venv/bin/pip install -e '.[test,macos]'
167
+ .venv/bin/pytest
168
+ cd js && npm ci && npm test
169
+ ```
170
+
171
+ The tests do not need any grant. They cover the argument parsing, the station server (run on a free port with a fake screen and a fake input sink), its input logic and recipe runner with a stand-in for Quartz, the path settings, the window and panel detection on recorded window lists, the typing logic with a fake helper, the ntfy logic, and the config plugin run against a fixture Android project. On macOS one test also compiles `sckshot` without signing it.
172
+
173
+ ## License
174
+
175
+ MIT
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "claude-human"
7
+ version = "0.2.0"
8
+ description = "Screen capture, lock screen typing and a phone view of one window, for a Mac that a person and an agent share"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Konstantinos Georgiou" }]
13
+ dependencies = []
14
+ keywords = ["macos", "screencapturekit", "screenshot", "karabiner", "virtual-hid", "claude", "remote"]
15
+ classifiers = ["Programming Language :: Python :: 3", "Operating System :: MacOS", "Topic :: Utilities"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/drkostas/claude-human"
19
+
20
+ [project.scripts]
21
+ claude-human = "claude_human.cli:main"
22
+
23
+ [project.optional-dependencies]
24
+ macos = ["pyobjc-framework-Quartz>=10; sys_platform == 'darwin'"]
25
+ test = ["pytest>=8"]
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
29
+
30
+ [tool.setuptools.package-data]
31
+ claude_human = ["tools/sckshot/*.swift", "tools/sckshot/*.sh", "tools/vhid/*.cpp", "tools/vhid/*.sh", "skill/SKILL.md"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """Tools for a person and an agent to share one Mac: screen capture, typing at the lock screen and
2
+ password panels through a virtual HID keyboard, and a station that hands one window to a phone."""
3
+
4
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())