T09 · Insecure Skill Coding Practices
- Location
scripts/sync.py:166- Finding
API-Controlled Meeting ID Allows Path Traversal Outside the Output Directory
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill does what it advertises, but it uses Granola app tokens to bulk export private meeting data on a recurring schedule without enough scoping, storage hardening, or safeguards.
Review before installing. Use a protected local directory, avoid shared or synced folders unless intended, understand that Granola app tokens are reused to download meeting content, and remove or avoid the cron job if you do not want ongoing background sync. The package should ideally validate meeting IDs, set restrictive file permissions, add network timeouts, pin dependencies, and document how to disable scheduled sync.
scripts/sync.py:166API-Controlled Meeting ID Allows Path Traversal Outside the Output Directory
scripts/sync.py:171Sensitive Meeting Records Are Written Without Restrictive Filesystem Permissions
SKILL.md:16Installation Instructions Use an Unpinned and Unverified Third-Party Dependency
scripts/sync.py:62Granola API Requests Can Block Indefinitely Because No Timeout Is Configured
The skill documentation directs users to install dependencies, run a Python sync script, and configure a recurring cron job that performs network access and writes meeting data to local disk, yet the skill declares no permissions. This undermines transparency and informed consent because users and reviewers are not explicitly told that the skill has ongoing network and file-write capabilities over potentially sensitive meeting content.
The setup instructions tell the user to sync cloud-hosted meeting transcripts and notes to a local directory and then schedule that sync every 6 hours, but they do not clearly warn that sensitive meeting content will be persistently written to disk on an ongoing basis. In the context of meeting transcripts and AI notes, this can expose confidential business or personal information to local compromise, backups, shared accounts, or accidental redistribution.
The documentation states that the sync script reads authentication from ~/Library/Application Support/Granola/supabase.json but does not explicitly warn that it is accessing locally stored auth tokens from the Granola app. Referencing an app credential store without clear disclosure is risky because it can normalize silent credential reuse and may surprise users who do not realize the skill is operating with bearer tokens tied to their account.
The script exports highly sensitive meeting content, attendee information, transcripts, and notes to local files by default with no consent prompt, access-control guidance, or minimization. In an agent skill context, this broad bulk export increases the chance of unintended disclosure through shared machines, insecure backups, other tools reading the output directory, or accidental onward sharing.
The script reads an access token from Granola's local authentication store and uses it to access meeting data in bulk. In an agent skill, local credential harvesting is sensitive because it bypasses an explicit re-auth flow and enables broad access to private meetings if the script is run in a context where the user did not understand that their stored token would be consumed.
def get_token():
"""Get access token from Granola's local auth file."""
if not SUPABASE_PATH.exists():
print(f"Error: Auth file not found at {SUPABASE_PATH}")
print("Make sure Granola is installed and you're signed in.")
The code extracts and validates the access token from local storage, confirming that the skill actively leverages a resident credential to impersonate the user to the Granola API. This is dangerous in a skill context because compromise or misuse of the skill grants access to all meetings available to that token without additional user authorization barriers.
token = tokens.get("access_token")
if not token:
print("Error: No access token found. Try signing into Granola again.")
sys.exit(1)
# Check expiration
No suspicious patterns detected.