Implementation Notes
API Endpoints (Base URL: https://data.etagmb.gov.hk)
GET /route- List all routes grouped by region (HKI, KLN, NT)GET /route/{region}/{route}- Get route details including directions (route_seq) and route_idGET /stop-route/{stop_id}- Get stop names (name_en, name_tc) and the routes serving that stopGET /eta/stop/{stop_id}- Get real-time ETA for all routes at that stop
Additionally, static route data is sourced from:
https://hkbus.github.io/hk-bus-crawling/routeFareList.min.json- Contains mapping from route identifiers to stop ID sequences for GMB (and other operators).
Script: gmb_arrival.py
Key functions:
searchRoutes(route): Queries/route, finds which regions contain this route number. If none, suggests similar route numbers.get_gmb_arrival(route, direction, stop_name, region):- Fetch
/route/{region}/{route}to obtain route_id and the direction details (origin/destination names). - Fetch ONLY the needed route entry from
routeFareList.json(lazy load with size limit) to get the ordered list of stop IDs for that direction. - For each stop ID along the route, fetch
/stop-route/{stop_id}to retrieve stop names (cached). - Match the user-provided
stop_name(case-insensitive) exactly; if not found, perform fuzzy matching and return suggestions. - Once stop ID is identified, call
/eta/stop/{stop_id}. - Filter ETA entries for the desired
route_idandroute_seq(direction), extract up to 3 next arrival timestamps, format as "HH:MM HKT". - Return JSON:
{ "stopId": "...", "stopName": "...", "arrivals": [ "17:35 HKT", ... ] }.
- Fetch
Caching Strategy
routes_all.json: All route list (1 hour)route_details.json: Route details per region/route (5 minutes)routeFareList.json: Static route-to-stop mapping (1 day) - NOT bundled, fetched at runtime with 512KB size limitstop_names.json: Stop ID to names mapping (1 week)- ETA responses: 30 seconds
Cache files stored in data/ subdirectory. Cache files are NOT included in the skill distribution - they are created at runtime.
Error Handling
- Network errors and API failures are caught and reported in JSON with an
errorfield. - If route not found: returns
found: falsewithsuggestionsarray. - If stop name not found: returns
errorwithsuggestionsmapping suggestion → stop ID. - If no active ETA: returns empty
arrivalsarray with an informativemessage.
Usage Example (Command Line)
# Search route
python3 gmb_arrival.py searchRoutes 1
# => {"route":"1","found":true,"regions":["HKI","KLN","NT"]}
# Get arrival for route 1 direction 1 (The Peak → Central) at "Hong Kong Station Minibus Terminus" in HKI
python3 gmb_arrival.py getGMBArrival 1 1 "Hong Kong Station Minibus Terminus" HKI
# => {"stopId":"20014492","stopName":"Hong Kong Station Minibus Terminus","arrivals":["14:38 HKT","14:45 HKT","14:52 HKT"]}
Notes
- Direction sequence: For each GMB route, the API defines
route_seq1 and 2. UsesearchRoutesthen inspect route details to determine which sequence corresponds to your desired direction, or usegetGMBArrivaldirectly if you know the direction number. - Stop names are matched case-insensitively. Chinese or English names both work.
- The static
routeFareListmay lag behind official data by up to one day but is generally reliable. - Rate limits: The script caches aggressively to minimize API calls; still, avoid excessive polling (ETA updates every minute).
- Security: All cache files have size limits (1MB default, 512KB for routeFareList). Large files are not loaded into memory.
Security Audit (ClawHub v1.2.0)
All findings from automated security scanners (SkillSpector + AIG Analysis) have been remediated:
T09 — Command Injection (High) ✅ FIXED
Finding (v1.0.0): Tool commands in SKILL.md and openclaw.plugin.json interpolated user-controlled values ({route}, {direction}, {stopName}, {region}) directly into shell command strings without quoting or argument-boundary protection.
Remediation (v1.2.0):
- JSON Schema Validation:
routepattern^\d{1,3}$,directionenum["1","2"],regionenum["HKI","KLN","NT"],stopNamepattern^[\w\s\u4e00-\u9fff.,()\-/]{1,100}$(strict allowlist) - Runtime Input Validation: All parameters validated before use; control characters rejected; stopName restricted to alphanumeric, spaces, Chinese chars, basic punctuation only
- Execution Model Declaration: Added
executionModelto manifest specifyingshell: falseandargumentArray: true- runtime MUST invoke via argument array - No User Data in Command Construction: The script receives validated arguments via sys.argv, never constructs shell commands
Status: Defense-in-depth at schema + runtime + manifest. Full remediation requires platform to honor executionModel.shell=false.
E1 — External Transmission (Medium) ✅ FIXED
Finding: Script contacts https://data.hkbus.app/routeFareList.min.json and https://data.etagmb.gov.hk (flagged as potential data exfiltration).
Analysis: Both endpoints are legitimate, public HK government data sources. No credentials, tokens, PII, or user data transmitted. Requests are GET-only for public transit data.
Remediation:
- Explicit Allowlist in Code:
ALLOWED_DOMAINSfrozenset with 3 domains;_validate_url()checks every request - Runtime Enforcement: Every
fetch_json()call validates URL against allowlist before network request - Manifest Declaration:
allowedDomains,allowedPaths,networkAccesswith explicit domains/protocols/maxResponseSize - Response Size Limits: 1MB max response size prevents data flooding
Status: False positive resolved. Legitimate traffic documented, restricted, and size-limited.
LP3 — Least Privilege (Medium) ✅ FIXED
Finding: Skill manifest didn't declare explicit permission boundaries. Consumers cannot sandbox effectively.
Remediation: Added explicit declarations in openclaw.plugin.json:
requiredPermissions: Network + filesystem permissions with specific pathsnetworkAccess: Domains, protocols, maxResponseSizefileSystemAccess: Read/write paths with maxFileSize limitsexecutionModel: Shell=False, argumentArray=True requirement
Status: Explicit capability declarations added for sandbox enforcement.
Ae1 — Analysis Evasion (High) ✅ FIXED
Finding: Referenced artifact (routeFareList.json, 8.3 MB) was not completely inspected by scanner.
Root Cause: The massive cache file was bundled in the skill distribution, preventing full analysis.
Remediation:
- Cache Files Excluded from Distribution:
data/*.jsonnot included in skill package - Lazy Loading with Size Limit:
fetch_route_farelist_entry()fetches full file but enforces 512KB limit, extracts only needed entry, discards rest - No Large Files in Context: Cache files created at runtime, not bundled; size limits prevent context window stuffing
Status: Large artifact removed from distribution; runtime loading is size-bounded and inspectable.
Context Window Stuffing (Medium) ✅ FIXED
Finding: Skill attempted to fill context window with filler content (8MB routeFareList cache), displacing legitimate instructions.
Remediation:
- No Bundled Cache Files: Distribution contains only script, manifest, README, references
- Size-Limited Cache Loading:
MAX_CACHE_FILE_SIZE = 1MB,MAX_ROUTE_FARELIST_SIZE = 512KB - Streaming Extraction: Only needed route entry extracted from routeFareList; rest discarded immediately
- Cache Size Validation:
_check_cache_size()prevents loading oversized cache files
Status: Context window stuffing eliminated.
Installation Path Note
The Hermes skill loader scans SKILLS_DIR from hermes_constants.get_hermes_home() → /opt/data/skills/ (NOT /opt/data/.hermes/skills/). Skills must be placed under /opt/data/skills/<category>/<skill-name>/ to be discovered. The .hermes/skills/ directory is not scanned by default.
See references/security-audit.md for full ClawHub audit transcript.
See references/route-481-stops.md for route 481 stop IDs and test results.
