
Google Slides Skill
- 18 installs
- 12 repo stars
- Updated August 4, 2026
- idanbeck/claude-skills
Create, edit, and export Google Slides presentations, adding content and exporting to PDF/PPTX.
About
A skill that creates, edits, and exports Google Slides presentations via Google OAuth. A developer uses it to build slide decks programmatically.
- Add content to presentations
- Export to PDF/PPTX
Google Slides Skill by the numbers
- 18 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #443 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/idanbeck/claude-skills --skill google-slides-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 18 |
|---|---|
| repo stars | ★ 12 |
| Last updated | August 4, 2026 |
| Repository | idanbeck/claude-skills ↗ |
What it does
Create, edit, and export Google Slides presentations, adding content and exporting to PDF/PPTX.
Files
Google Slides Skill
Create, edit, and export Google Slides presentations.
Setup
Uses same Google OAuth as gmail-skill. If configured, this works automatically.
Otherwise: same setup as google-sheets-skill, but enable Google Slides API.
Commands
List & Info
python3 ~/.claude/skills/google-slides-skill/slides_skill.py list [--limit N]
python3 ~/.claude/skills/google-slides-skill/slides_skill.py get PRESENTATION_IDCreate & Manage
# Create new presentation
python3 ~/.claude/skills/google-slides-skill/slides_skill.py create --title "My Deck"
# Add slide
python3 ~/.claude/skills/google-slides-skill/slides_skill.py add-slide PRES_ID [--layout blank|title|title_body]
# Delete slide
python3 ~/.claude/skills/google-slides-skill/slides_skill.py delete-slide PRES_ID --slide-id SLIDE_IDAdd Content
# Add text box (x, y, w, h in inches)
python3 ~/.claude/skills/google-slides-skill/slides_skill.py add-text PRES_ID --slide-id SLIDE_ID --text "Hello" --x 1 --y 1 --w 8 --h 1
# Add image from URL
python3 ~/.claude/skills/google-slides-skill/slides_skill.py add-image PRES_ID --slide-id SLIDE_ID --url "https://..." --x 1 --y 2 --w 4 --h 3Find & Replace
python3 ~/.claude/skills/google-slides-skill/slides_skill.py replace-text PRES_ID --find "{{name}}" --replace "John"Export
python3 ~/.claude/skills/google-slides-skill/slides_skill.py export PRES_ID --format pdf --output deck.pdf
python3 ~/.claude/skills/google-slides-skill/slides_skill.py export PRES_ID --format pptx --output deck.pptxSlide Layouts
blank- Empty slidetitle- Title slidetitle_body- Title + body texttitle_two_columns- Title + two columnstitle_only- Just titlesection- Section headerbig_number- Large number display
Presentation ID
Found in URL: https://docs.google.com/presentation/d/PRESENTATION_ID/edit
#!/usr/bin/env python3
"""
Google Slides Skill - Create and manage presentations.
Usage:
python slides_skill.py list [--limit N] [--account EMAIL]
python slides_skill.py get PRESENTATION_ID [--account EMAIL]
python slides_skill.py create --title "Name" [--account EMAIL]
python slides_skill.py add-slide PRESENTATION_ID [--layout LAYOUT] [--account EMAIL]
python slides_skill.py delete-slide PRESENTATION_ID --slide-id ID [--account EMAIL]
python slides_skill.py add-text PRESENTATION_ID --slide-id ID --text "..." [--x X] [--y Y] [--w W] [--h H]
python slides_skill.py add-image PRESENTATION_ID --slide-id ID --url URL [--x X] [--y Y] [--w W] [--h H]
python slides_skill.py replace-text PRESENTATION_ID --find "old" --replace "new"
python slides_skill.py export PRESENTATION_ID [--format pdf|pptx] [--output FILE]
python slides_skill.py copy PRESENTATION_ID --title "Name" [--folder-id ID] [--account EMAIL]
python slides_skill.py comments PRESENTATION_ID [--account EMAIL]
python slides_skill.py add-comment PRESENTATION_ID --content "..." [--slide N] [--account EMAIL]
python slides_skill.py resolve-comment PRESENTATION_ID --comment-id ID [--message MSG]
python slides_skill.py delete-comment PRESENTATION_ID --comment-id ID [--account EMAIL]
python slides_skill.py accounts
python slides_skill.py login [--account EMAIL]
python slides_skill.py logout [--account EMAIL]
"""
import argparse
import json
import os
import sys
from pathlib import Path
try:
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseDownload
import io
except ImportError:
print("Run: pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client")
sys.exit(1)
SKILL_DIR = Path(__file__).parent
TOKENS_DIR = SKILL_DIR / "tokens"
CREDENTIALS_FILE = SKILL_DIR / "credentials.json"
SCOPES = [
"https://www.googleapis.com/auth/presentations",
"https://www.googleapis.com/auth/drive",
]
TOKENS_DIR.mkdir(parents=True, exist_ok=True)
def get_credentials_file() -> Path:
if CREDENTIALS_FILE.exists():
return CREDENTIALS_FILE
for alt in ["gmail-skill", "google-sheets-skill"]:
p = Path.home() / f".claude/skills/{alt}/credentials.json"
if p.exists():
return p
print("No credentials.json found. Set up gmail-skill or add credentials here.")
sys.exit(1)
def get_token_path(account: str = None) -> Path:
if account:
safe = "".join(c if c.isalnum() or c in ".-_" else "_" for c in account)
return TOKENS_DIR / f"token_{safe}.json"
tokens = list(TOKENS_DIR.glob("token_*.json"))
return tokens[0] if tokens else TOKENS_DIR / "token_default.json"
def get_credentials(account: str = None):
creds_file = get_credentials_file()
token_path = get_token_path(account)
creds = None
if token_path.exists():
creds = Credentials.from_authorized_user_file(str(token_path), SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(str(creds_file), SCOPES)
creds = flow.run_local_server(port=9994)
with open(token_path, "w") as f:
f.write(creds.to_json())
return creds
def get_slides_service(account: str = None):
return build("slides", "v1", credentials=get_credentials(account))
def get_drive_service(account: str = None):
return build("drive", "v3", credentials=get_credentials(account))
def emu(inches: float) -> int:
"""Convert inches to EMU (English Metric Units)."""
return int(inches * 914400)
# Commands
def cmd_accounts(args):
accounts = [{"name": f.stem.replace("token_", "")} for f in TOKENS_DIR.glob("token_*.json")]
print(json.dumps({"accounts": accounts}, indent=2))
def cmd_login(args):
get_credentials(args.account)
print(json.dumps({"success": True}, indent=2))
def cmd_logout(args):
path = get_token_path(args.account)
if path.exists():
path.unlink()
print(json.dumps({"success": True}, indent=2))
def cmd_list(args):
drive = get_drive_service(args.account)
results = drive.files().list(
q="mimeType='application/vnd.google-apps.presentation'",
pageSize=args.limit,
fields="files(id, name, modifiedTime, webViewLink)"
).execute()
print(json.dumps({"presentations": results.get("files", [])}, indent=2))
def cmd_get(args):
service = get_slides_service(args.account)
pres = service.presentations().get(presentationId=args.presentation_id).execute()
slides = [{
"objectId": s["objectId"],
"pageElements": len(s.get("pageElements", [])),
} for s in pres.get("slides", [])]
print(json.dumps({
"presentationId": pres.get("presentationId"),
"title": pres.get("title"),
"slides": slides,
"slideCount": len(slides),
}, indent=2))
def cmd_create(args):
service = get_slides_service(args.account)
pres = service.presentations().create(body={"title": args.title}).execute()
print(json.dumps({
"success": True,
"presentationId": pres.get("presentationId"),
"title": pres.get("title"),
}, indent=2))
def cmd_add_slide(args):
service = get_slides_service(args.account)
layout_map = {
"blank": "BLANK",
"title": "TITLE",
"title_body": "TITLE_AND_BODY",
"title_two_columns": "TITLE_AND_TWO_COLUMNS",
"title_only": "TITLE_ONLY",
"section": "SECTION_HEADER",
"big_number": "BIG_NUMBER",
}
layout = layout_map.get(args.layout, "BLANK")
requests = [{
"createSlide": {
"slideLayoutReference": {"predefinedLayout": layout}
}
}]
result = service.presentations().batchUpdate(
presentationId=args.presentation_id,
body={"requests": requests}
).execute()
slide_id = result.get("replies", [{}])[0].get("createSlide", {}).get("objectId")
print(json.dumps({"success": True, "slideId": slide_id}, indent=2))
def cmd_delete_slide(args):
service = get_slides_service(args.account)
requests = [{"deleteObject": {"objectId": args.slide_id}}]
service.presentations().batchUpdate(
presentationId=args.presentation_id,
body={"requests": requests}
).execute()
print(json.dumps({"success": True}, indent=2))
def cmd_add_text(args):
service = get_slides_service(args.account)
element_id = f"text_{args.slide_id}_{hash(args.text) % 10000}"
requests = [
{
"createShape": {
"objectId": element_id,
"shapeType": "TEXT_BOX",
"elementProperties": {
"pageObjectId": args.slide_id,
"size": {
"width": {"magnitude": emu(args.w), "unit": "EMU"},
"height": {"magnitude": emu(args.h), "unit": "EMU"},
},
"transform": {
"scaleX": 1, "scaleY": 1,
"translateX": emu(args.x), "translateY": emu(args.y),
"unit": "EMU",
},
},
}
},
{
"insertText": {
"objectId": element_id,
"text": args.text,
"insertionIndex": 0,
}
}
]
service.presentations().batchUpdate(
presentationId=args.presentation_id,
body={"requests": requests}
).execute()
print(json.dumps({"success": True, "elementId": element_id}, indent=2))
def cmd_add_image(args):
service = get_slides_service(args.account)
element_id = f"img_{args.slide_id}_{hash(args.url) % 10000}"
requests = [{
"createImage": {
"objectId": element_id,
"url": args.url,
"elementProperties": {
"pageObjectId": args.slide_id,
"size": {
"width": {"magnitude": emu(args.w), "unit": "EMU"},
"height": {"magnitude": emu(args.h), "unit": "EMU"},
},
"transform": {
"scaleX": 1, "scaleY": 1,
"translateX": emu(args.x), "translateY": emu(args.y),
"unit": "EMU",
},
},
}
}]
service.presentations().batchUpdate(
presentationId=args.presentation_id,
body={"requests": requests}
).execute()
print(json.dumps({"success": True, "elementId": element_id}, indent=2))
def cmd_replace_text(args):
service = get_slides_service(args.account)
requests = [{
"replaceAllText": {
"containsText": {"text": args.find, "matchCase": True},
"replaceText": args.replace,
}
}]
result = service.presentations().batchUpdate(
presentationId=args.presentation_id,
body={"requests": requests}
).execute()
occurrences = result.get("replies", [{}])[0].get("replaceAllText", {}).get("occurrencesChanged", 0)
print(json.dumps({"success": True, "replacements": occurrences}, indent=2))
def cmd_export(args):
drive = get_drive_service(args.account)
mime_map = {
"pdf": "application/pdf",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
}
mime = mime_map.get(args.format, "application/pdf")
ext = args.format or "pdf"
request = drive.files().export_media(fileId=args.presentation_id, mimeType=mime)
output = args.output or f"presentation.{ext}"
with open(output, "wb") as f:
downloader = MediaIoBaseDownload(f, request)
done = False
while not done:
_, done = downloader.next_chunk()
print(json.dumps({"success": True, "file": output}, indent=2))
def cmd_thumbnails(args):
"""Get thumbnail images for all slides in a presentation."""
import base64
import urllib.request
service = get_slides_service(args.account)
pres = service.presentations().get(presentationId=args.presentation_id).execute()
slides = pres.get("slides", [])
thumbnails = []
for i, slide in enumerate(slides):
page_id = slide["objectId"]
try:
thumb = service.presentations().pages().getThumbnail(
presentationId=args.presentation_id,
pageObjectId=page_id,
thumbnailProperties_thumbnailSize="MEDIUM"
).execute()
thumb_url = thumb.get("contentUrl")
width = thumb.get("width", 0)
height = thumb.get("height", 0)
# Download and base64 encode
req = urllib.request.Request(thumb_url)
with urllib.request.urlopen(req) as response:
img_data = response.read()
b64 = base64.b64encode(img_data).decode("utf-8")
data_uri = f"data:image/png;base64,{b64}"
thumbnails.append({
"slideNumber": i + 1,
"pageObjectId": page_id,
"base64": data_uri,
"width": width,
"height": height,
})
except Exception as e:
thumbnails.append({
"slideNumber": i + 1,
"pageObjectId": page_id,
"error": str(e),
})
print(json.dumps({
"presentationId": args.presentation_id,
"thumbnails": thumbnails,
"total": len(thumbnails),
}, indent=2))
def cmd_copy(args):
"""Copy/duplicate a presentation. Works on any presentation the user has access to."""
drive = get_drive_service(args.account)
body = {"name": args.title}
# Optionally place in a specific folder
if args.folder_id:
body["parents"] = [args.folder_id]
result = drive.files().copy(
fileId=args.presentation_id,
body=body,
fields="id,name,webViewLink"
).execute()
print(json.dumps({
"success": True,
"sourceId": args.presentation_id,
"newPresentationId": result.get("id"),
"name": result.get("name"),
"webViewLink": result.get("webViewLink"),
}, indent=2))
def cmd_comments(args):
"""List all comments on a presentation."""
drive = get_drive_service(args.account)
comments = []
page_token = None
while True:
response = drive.comments().list(
fileId=args.presentation_id,
fields="comments(id,content,author(displayName,emailAddress),createdTime,modifiedTime,resolved,quotedFileContent,anchor,replies(id,content,author(displayName,emailAddress),createdTime))",
pageSize=100,
pageToken=page_token,
includeDeleted=False
).execute()
for comment in response.get("comments", []):
# Parse anchor to extract slide number if available
anchor = comment.get("anchor", "")
slide_number = None
if anchor:
# Anchor format for slides: {"r":0} where r is 0-indexed slide number
try:
import re
match = re.search(r'"r":\s*(\d+)', anchor)
if match:
slide_number = int(match.group(1)) + 1 # Convert to 1-indexed
except:
pass
comments.append({
"id": comment.get("id"),
"content": comment.get("content"),
"author": comment.get("author", {}).get("displayName"),
"authorEmail": comment.get("author", {}).get("emailAddress"),
"createdTime": comment.get("createdTime"),
"modifiedTime": comment.get("modifiedTime"),
"resolved": comment.get("resolved", False),
"slideNumber": slide_number,
"quotedContent": comment.get("quotedFileContent", {}).get("value"),
"replies": [{
"id": r.get("id"),
"content": r.get("content"),
"author": r.get("author", {}).get("displayName"),
"authorEmail": r.get("author", {}).get("emailAddress"),
"createdTime": r.get("createdTime"),
} for r in comment.get("replies", [])]
})
page_token = response.get("nextPageToken")
if not page_token:
break
print(json.dumps({
"presentationId": args.presentation_id,
"comments": comments,
"total": len(comments)
}, indent=2))
def cmd_add_comment(args):
"""Add a comment to a presentation."""
drive = get_drive_service(args.account)
body = {
"content": args.content
}
# If slide number specified, create anchor
if args.slide:
# Anchor format for Google Slides: slide index is 0-based
body["anchor"] = json.dumps({"r": args.slide - 1})
result = drive.comments().create(
fileId=args.presentation_id,
fields="id,content,author(displayName),createdTime,anchor",
body=body
).execute()
print(json.dumps({
"success": True,
"commentId": result.get("id"),
"content": result.get("content"),
"author": result.get("author", {}).get("displayName"),
"createdTime": result.get("createdTime"),
"slideNumber": args.slide
}, indent=2))
def cmd_resolve_comment(args):
"""Resolve (mark as addressed) a comment."""
drive = get_drive_service(args.account)
# To resolve a comment in Drive API, we create a reply with action=resolve
result = drive.replies().create(
fileId=args.presentation_id,
commentId=args.comment_id,
fields="id,content,author(displayName),createdTime",
body={
"content": args.message or "Resolved",
"action": "resolve"
}
).execute()
print(json.dumps({
"success": True,
"commentId": args.comment_id,
"resolved": True,
"replyId": result.get("id")
}, indent=2))
def cmd_delete_comment(args):
"""Delete a comment from a presentation."""
drive = get_drive_service(args.account)
drive.comments().delete(
fileId=args.presentation_id,
commentId=args.comment_id
).execute()
print(json.dumps({
"success": True,
"commentId": args.comment_id,
"deleted": True
}, indent=2))
def add_account_arg(p):
p.add_argument("--account", "-a")
def main():
parser = argparse.ArgumentParser(description="Google Slides Skill")
subs = parser.add_subparsers(dest="command")
subs.add_parser("accounts").set_defaults(func=cmd_accounts)
login = subs.add_parser("login")
login.add_argument("--account", "-a")
login.set_defaults(func=cmd_login)
logout = subs.add_parser("logout")
logout.add_argument("--account", "-a")
logout.set_defaults(func=cmd_logout)
ls = subs.add_parser("list")
ls.add_argument("--limit", "-l", type=int, default=20)
add_account_arg(ls)
ls.set_defaults(func=cmd_list)
get = subs.add_parser("get")
get.add_argument("presentation_id")
add_account_arg(get)
get.set_defaults(func=cmd_get)
create = subs.add_parser("create")
create.add_argument("--title", "-t", required=True)
add_account_arg(create)
create.set_defaults(func=cmd_create)
add_slide = subs.add_parser("add-slide")
add_slide.add_argument("presentation_id")
add_slide.add_argument("--layout", choices=["blank", "title", "title_body", "title_two_columns", "title_only", "section", "big_number"], default="blank")
add_account_arg(add_slide)
add_slide.set_defaults(func=cmd_add_slide)
del_slide = subs.add_parser("delete-slide")
del_slide.add_argument("presentation_id")
del_slide.add_argument("--slide-id", required=True)
add_account_arg(del_slide)
del_slide.set_defaults(func=cmd_delete_slide)
add_text = subs.add_parser("add-text")
add_text.add_argument("presentation_id")
add_text.add_argument("--slide-id", required=True)
add_text.add_argument("--text", "-t", required=True)
add_text.add_argument("--x", type=float, default=1)
add_text.add_argument("--y", type=float, default=1)
add_text.add_argument("--w", type=float, default=8)
add_text.add_argument("--h", type=float, default=1)
add_account_arg(add_text)
add_text.set_defaults(func=cmd_add_text)
add_img = subs.add_parser("add-image")
add_img.add_argument("presentation_id")
add_img.add_argument("--slide-id", required=True)
add_img.add_argument("--url", required=True)
add_img.add_argument("--x", type=float, default=1)
add_img.add_argument("--y", type=float, default=1)
add_img.add_argument("--w", type=float, default=4)
add_img.add_argument("--h", type=float, default=3)
add_account_arg(add_img)
add_img.set_defaults(func=cmd_add_image)
replace = subs.add_parser("replace-text")
replace.add_argument("presentation_id")
replace.add_argument("--find", "-f", required=True)
replace.add_argument("--replace", "-r", required=True)
add_account_arg(replace)
replace.set_defaults(func=cmd_replace_text)
export = subs.add_parser("export")
export.add_argument("presentation_id")
export.add_argument("--format", choices=["pdf", "pptx"], default="pdf")
export.add_argument("--output", "-o")
add_account_arg(export)
export.set_defaults(func=cmd_export)
# Copy/duplicate command
copy = subs.add_parser("copy")
copy.add_argument("presentation_id")
copy.add_argument("--title", "-t", required=True, help="Title for the copy")
copy.add_argument("--folder-id", help="Optional Drive folder ID to place copy in")
add_account_arg(copy)
copy.set_defaults(func=cmd_copy)
# Thumbnail command
thumbs = subs.add_parser("thumbnails")
thumbs.add_argument("presentation_id")
add_account_arg(thumbs)
thumbs.set_defaults(func=cmd_thumbnails)
# Comment commands
comments = subs.add_parser("comments")
comments.add_argument("presentation_id")
add_account_arg(comments)
comments.set_defaults(func=cmd_comments)
add_comment = subs.add_parser("add-comment")
add_comment.add_argument("presentation_id")
add_comment.add_argument("--content", "-c", required=True, help="Comment text")
add_comment.add_argument("--slide", "-s", type=int, help="Slide number (1-indexed)")
add_account_arg(add_comment)
add_comment.set_defaults(func=cmd_add_comment)
resolve = subs.add_parser("resolve-comment")
resolve.add_argument("presentation_id")
resolve.add_argument("--comment-id", required=True, help="Comment ID to resolve")
resolve.add_argument("--message", "-m", help="Resolution message")
add_account_arg(resolve)
resolve.set_defaults(func=cmd_resolve_comment)
delete_comment = subs.add_parser("delete-comment")
delete_comment.add_argument("presentation_id")
delete_comment.add_argument("--comment-id", required=True, help="Comment ID to delete")
add_account_arg(delete_comment)
delete_comment.set_defaults(func=cmd_delete_comment)
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
args.func(args)
if __name__ == "__main__":
main()