# Environment variables

> PHONE_HARNESS_BACKGROUND default and falsey values, PH_AGENT_WORKSPACE path override, temp capture directory under phone-harness, and how backend selection falls back when SkyLight load fails.

- Repository: ShawnPana/phone-harness
- GitHub: https://github.com/ShawnPana/phone-harness
- Human docs: https://grok-wiki.com/public/docs/shawnpana-phone-harness-bf80173a2a2e
- Complete Markdown: https://grok-wiki.com/public/docs/shawnpana-phone-harness-bf80173a2a2e/llms-full.txt

## Source Files

- `src/phone_harness/helpers.py`
- `src/phone_harness/background.py`
- `src/phone_harness/mirror.py`
- `agent-workspace/agent_helpers.py`
- `src/phone_harness/run.py`

---

---
title: "Environment variables"
description: "PHONE_HARNESS_BACKGROUND default and falsey values, PH_AGENT_WORKSPACE path override, temp capture directory under phone-harness, and how backend selection falls back when SkyLight load fails."
---

phone-harness reads two environment variables at `helpers` import time: `PHONE_HARNESS_BACKGROUND` selects the input/capture backend (background SkyLight path vs classic mirror), and `PH_AGENT_WORKSPACE` points at the directory whose `agent_helpers.py` is auto-merged into the script namespace. Capture defaults land under a fixed `{tempdir}/phone-harness` directory (not env-configurable). Backend choice is fixed for the process once `phone_harness.helpers` loads.

## Variable reference

| Variable | Default | Read when | Effect |
|---|---|---|---|
| `PHONE_HARNESS_BACKGROUND` | `"1"` | Import of `phone_harness.helpers` | Prefer the background backend (`background.py`) when truthy; force classic mirror (`mirror.py`) when falsey |
| `PH_AGENT_WORKSPACE` | `<repo-root>/agent-workspace` | Same import | Directory containing `agent_helpers.py` for agent-editable helpers |

No other environment variables are consulted by the package for backend selection, workspace loading, or default capture paths. `phone-harness --doctor` imports `mirror` directly and does not honor `PHONE_HARNESS_BACKGROUND`.

## `PHONE_HARNESS_BACKGROUND`

### Selection logic

In `src/phone_harness/helpers.py`:

```python
_BACKGROUND = os.environ.get("PHONE_HARNESS_BACKGROUND", "1").lower() not in (
    "0", "false", "no")
if _BACKGROUND:
    try:
        mirror = importlib.import_module(".background", __package__)
    except Exception:
        mirror = importlib.import_module(".mirror", __package__)
        _BACKGROUND = False
else:
    mirror = importlib.import_module(".mirror", __package__)
```

After selection, helpers bind transport primitives from the chosen module:

- `tap`, `long_press`, `drag`, `press`, `type_text`, `activate`, `find_window`
- Higher-level helpers call that module’s `capture`, `ensure_window`, `scroll_wheel`, and related APIs via the local `mirror` alias

### Defaults and falsey values

| Value (after `.lower()`) | Result |
|---|---|
| unset | Background on (default `"1"`) |
| `"1"`, `"true"`, `"yes"`, or any string **not** in the falsey set | Background attempted |
| `"0"`, `"false"`, `"no"` | Classic mirror forced |
| empty string `""` | Treated as truthy (not in the falsey set) |

Matching is case-insensitive only via `.lower()`; there is no stripping of whitespace.

### What each backend does

| Backend | Module | Focus behavior | Capture default path | Input path |
|---|---|---|---|---|
| Background (default) | `background.py` | Does not steal focus (`activate` is a no-op) | `{tempdir}/phone-harness/background.png` | SkyLight `SLPSPostEventRecordTo` mouse records; keyboard via make-key + `CGEventPostToPid` |
| Classic mirror | `mirror.py` | Activates iPhone Mirroring for capture/input | `{tempdir}/phone-harness/window.png` | `screencapture` + HID `CGEvent` posting; window must be frontmost or events are swallowed |

Both backends share the same global screen-point coordinate convention, so helpers such as `tap_text`, `swipe`, and `scroll_collect` keep the same API regardless of backend.

### Automatic fallback when SkyLight load fails

If background is requested and **any** exception is raised while importing `background` (including loading private SkyLight symbols at module top level), helpers:

1. Import `mirror` instead
2. Set `_BACKGROUND = False`
3. Continue without raising to the caller

There is no log line, stderr message, or public flag exposing that fallback occurred. SkyLight symbols are “not guaranteed across macOS builds”; the silent fall-through is intentional so the harness stays usable.

```text
PHONE_HARNESS_BACKGROUND truthy?
        │
        ├─ no ──► import mirror.py (classic)
        │
        └─ yes ─► try import background.py
                      │
                      ├─ success ──► use background (no focus steal)
                      │
                      └─ Exception ─► import mirror.py, _BACKGROUND=False
```

### Process lifetime

Selection runs once when `helpers` is first imported (for example when `phone-harness` runs `from . import helpers` in `run.py`). Changing the variable in the shell after a long-lived process has already imported helpers does not re-select the backend. Start a new `phone-harness` invocation to apply a new value.

### Common shell patterns

```bash
# Default: background backend (omit the variable)
phone-harness <<'PY'
print(screen_info())
PY

# Force classic mirror (focus-stealing HID path)
PHONE_HARNESS_BACKGROUND=0 phone-harness <<'PY'
print(screen_info())
PY

# Explicit background request (same as default when load succeeds)
PHONE_HARNESS_BACKGROUND=1 phone-harness <<'PY'
print(screen_info())
PY
```

<Warning>
`phone-harness --doctor` always uses `from . import mirror` (classic path for its capture probe). A doctor PASS does not prove the background backend loaded successfully for script runs.
</Warning>

## `PH_AGENT_WORKSPACE`

### Resolution

```python
CORE_DIR = Path(__file__).resolve().parent          # .../src/phone_harness
REPO_ROOT = CORE_DIR.parent.parent                  # checkout root
AGENT_WORKSPACE = Path(
    os.environ.get("PH_AGENT_WORKSPACE", REPO_ROOT / "agent-workspace"))
```

Default layout (relative to the installed or checked-out repo root):

```text
<repo-root>/
  agent-workspace/
    agent_helpers.py
  src/phone_harness/
    helpers.py
```

### Load behavior

At the end of `helpers` import, `_load_agent_helpers()`:

1. Looks for `{AGENT_WORKSPACE}/agent_helpers.py`
2. Returns immediately if the file is missing (no error)
3. Otherwise loads it via `importlib.util.spec_from_file_location`
4. Copies every name that does **not** start with `_` into `helpers` globals

The CLI (`run.py`) builds the script namespace from public names on `helpers`, so agent helpers are available in stdin scripts next to core APIs:

```bash
PH_AGENT_WORKSPACE=/path/to/my-workspace phone-harness <<'PY'
# tap_icon comes from agent_helpers.py when defined there
tap_icon("Weather")
PY
```

Stock `agent-workspace/agent_helpers.py` defines `tap_icon` (Home Screen icon: tap ~35 points above the OCR label). Edit that file (or an alternate workspace) for task-specific primitives.

<Note>
Override must be a directory path. The loader always appends `agent_helpers.py`; do not point `PH_AGENT_WORKSPACE` at the file itself.
</Note>

## Temp capture directory

There is **no** environment variable for capture storage. Both backends hardcode:

```python
TMP = Path(tempfile.gettempdir()) / "phone-harness"
TMP.mkdir(exist_ok=True)
```

| Item | Value |
|---|---|
| Root | `{tempfile.gettempdir()}/phone-harness` |
| Classic default file | `window.png` |
| Background default file | `background.png` |
| Directory creation | `mkdir(exist_ok=True)` on module import |

On macOS, `tempfile.gettempdir()` is typically under `/var/folders/.../T` (session-specific), so the full path is machine- and user-session-specific rather than a fixed `/tmp/phone-harness`.

### Overriding the file path (not the directory)

`capture(path=None, ...)` and helper `screenshot(path=None)` accept an explicit path. Only the default path uses `TMP`; callers can write elsewhere:

```python
screenshot("/tmp/my-phone.png")
```

Doctor capture uses a separate `tempfile.NamedTemporaryFile(suffix=".png")` and deletes it after the check; it does not rely on the `phone-harness` temp subdirectory.

## Operational notes

| Concern | Behavior |
|---|---|
| Backend switch mid-session | Not supported; restart the process |
| Background import failure | Silent fall-back to classic mirror |
| Missing `agent_helpers.py` | Silent skip; core helpers still work |
| Invalid agent helper syntax | Import of `helpers` fails (load is not wrapped in try/except) |
| Doctor vs scripts | Doctor always probes via classic `mirror`; scripts use selected backend |
| Permissions | Accessibility + Screen Recording still required for both backends |

## Failure modes

| Symptom | Likely cause | Check |
|---|---|---|
| Automation steals focus despite wanting background | `PHONE_HARNESS_BACKGROUND` is `0`/`false`/`no`, or SkyLight import fell back | Unset or set to `1`; inspect whether private framework load fails on this macOS build |
| Custom helpers not visible in scripts | Wrong `PH_AGENT_WORKSPACE`, missing file, or names start with `_` | Confirm `{PH_AGENT_WORKSPACE}/agent_helpers.py` exists and public names export |
| Capture PNGs hard to find | Looking under `/tmp` only | Resolve `tempfile.gettempdir()` and open `phone-harness/` under it |
| Taps silent on classic path | Window not frontmost or Accessibility denied | Grant Accessibility; classic backend activates; see troubleshooting |

## Related pages

<CardGroup>
  <Card title="Input backends" href="/input-backends">
    Background SkyLight path versus classic CGEvent mirror path, focus behavior, and when to force either mode.
  </Card>
  <Card title="Extend agent helpers" href="/extend-agent-helpers">
    Editing `agent_helpers.py`, `PH_AGENT_WORKSPACE`, auto-load into the script namespace, and `tap_icon`.
  </Card>
  <Card title="Helpers API" href="/helpers-api">
    Public helpers pre-imported into `phone-harness` scripts, including capture and gesture signatures.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Stdin Python exec, `--doctor`, `skill`, and how the script namespace is built from `helpers`.
  </Card>
  <Card title="Doctor diagnostics" href="/doctor-diagnostics">
    Ordered permission and session checks (classic mirror probe path).
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Blank capture, silent taps, blocked interstitials, and focus-related failures.
  </Card>
</CardGroup>
