Claude Code hook: swap Sume's download URL for a saved file
A PostToolUse hook with updatedToolOutput can fetch the signed link from assets_download_url, save the file, and show Claude only the local path.

Yes: a Claude Code PostToolUse hook can replace the result of an MCP tool before Claude sees it, using the updatedToolOutput field. For Sume that fits assets_download_url, which returns a short-lived signed link: the hook can download the file, save it locally, and hand Claude a file path instead of the link.
The hooks reference (read 2026-10-10) documents both updatedToolOutput, which works for all tools, and updatedMCPToolOutput, which is MCP-only and which the page says to avoid in favor of the first. The Claude Code changelog also lists a 2.1.296 fix for updatedMCPToolOutput not applying in some managed-settings sessions, so check your version if a rewrite seems ignored.
Why rewrite this one tool
Sume's tool description for assets_download_url calls its result a sensitive short-lived URL, to be used only for the requested download and not pasted into chat reports. The same description says that only ready first-party uploads can be downloaded this way; remote URLs registered with assets_create are not proxied.
A hook enforces that rule mechanically. The link is used inside your own process, and what Claude sees, and can repeat in a summary, is a path and the expiry time.
What the hooks page says you can and cannot do
Three caveats from the page decide how far to trust this. The tool has already run when the hook fires, so the rewrite changes what Claude sees, not what happened. Telemetry such as OpenTelemetry tool spans captures the original output before the hook runs, so the signed link can still appear there. And MCP tool output is passed through without schema validation, so a wrong shape is not rejected for you; it is simply what Claude reads.
| Field | Applies to | Note from the page |
|---|---|---|
updatedToolOutput | All tools | Replaces the output; value must match the tool's output shape |
updatedMCPToolOutput | MCP tools only | Page says to prefer updatedToolOutput |
additionalContext | All tools | Adds a string next to the result |
| Telemetry | All tools | Records the original output before the hook runs |
The hook
Register it in .claude/settings.json under PostToolUse with the matcher mcp__sume__assets_download_url, which assumes you added the server as sume the way Sume's quickstart does (claude mcp add --transport http sume https://mcp.sume.com/mcp). The hooks page says MCP tools are named mcp__<server>__<tool>; a plugin-bundled server uses a longer scoped name, so adjust the matcher if so.
The script reads the event from stdin and walks tool_response, including JSON inside text blocks. Where it finds an object with a download.url that starts with https://, it saves the bytes under .sume-downloads/ and writes back only the path and expires_at. The data.download.url shape comes from Sume's asset download test; if your session prints a different shape, log the event once and adjust the walk.
import json, os, sys, urllib.request
def fetch(node, out_dir):
if isinstance(node, str):
try:
return json.dumps(fetch(json.loads(node), out_dir))
except ValueError:
return node
if isinstance(node, list):
return [fetch(n, out_dir) for n in node]
if not isinstance(node, dict):
return node
node = {k: fetch(v, out_dir) for k, v in node.items()}
dl = node.get("download")
if isinstance(dl, dict) and str(dl.get("url", "")).startswith("https://"):
name = os.path.basename(str(node.get("asset", {}).get("id", "asset")))
os.makedirs(out_dir, exist_ok=True)
path = os.path.join(out_dir, name)
urllib.request.urlretrieve(dl["url"], path)
node["download"] = {"saved_to": path, "expires_at": dl.get("expires_at")}
return node
event = json.load(sys.stdin)
if event.get("tool_name") == "mcp__sume__assets_download_url":
new = fetch(event["tool_response"], ".sume-downloads")
if new != event["tool_response"]:
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "PostToolUse", "updatedToolOutput": new}}))Limits and a safe rollout
Run it on a throwaway asset first and read the saved file. The hook downloads without checking size, so add a cap before using it on large videos. It also only rewrites when it finds a signed link; any other result passes through unchanged, because the script prints nothing in that case.
A hook does not reduce what the tool can do. Pair it with a PreToolUse rule for paid Sume tools if the same session can create work, and keep dry_run and idempotency_key in the instructions, as Sume's tools-and-gates page describes.
- Matcher must match the full tool name; a bare server prefix matches nothing.
- Keep the key out of the hook; it uses the link Sume already signed.
- Treat OpenTelemetry output as possibly containing the original link.
- Re-run
assets_download_urlfor a new link afterexpires_atpasses.
Sources
Related posts
More in Developers
- Clear a full Sume queue: cancel queued jobs after 429 queue_full
When submits fail with 429 queue_full, list queued jobs with GET /v1/jobs?status=queued and cancel the ones you no longer need. Safe on a 409, with a script.
- Empty job_id next to job_ids: how Sume MCP treats placeholders
Some agent clients fill every optional tool field with empty strings, zeros and empty arrays. What Sume's MCP drops, what it keeps, and what still errors.
- Crash-safe Sume submit: write the key first, reconcile on boot
If a worker dies between POST and saving the job id, list queued and processing jobs, match idempotency_key, and resubmit only keys still unknown.
- Detect Sume OpenAPI drift in CI: hash the operations you call
Fetch api.sume.com/reference/json, hash only the operations you use, and fail CI when one changes. A 23-line script, plus the User-Agent a stdlib fetch needs.
Written by Sume