Apr 23, 2026
Latest updates and announcements
SDK file upload hardening: sensitive path blocking and upload hooks
The Composio SDKs add defense-in-depth for automatic file uploads (when a tool input is marked file_uploadable and the SDK reads a local path and sends the file to Composio storage). The goal is to reduce the risk of agent or app code accidentally exfiltrating secrets, SSH keys, or project env files that sit under well-known paths on disk.
URLs and File objects are not subject to the path-based denylist in the same way as string paths; the checks apply to resolved local filesystem paths used for auto-upload and the programmatic upload helpers that accept paths.
SDK versions
| SDK | Version (includes this behavior) |
|---|---|
| TypeScript | @composio/core v0.6.11 or later |
| Python | composio v0.11.6 or later |
What changed
- Default sensitive path protection — Before reading and uploading, the SDK checks the resolved local path against a built-in list of path segments (for example
.ssh,.aws,.claude,.kube) and file-name patterns (for example.env, default SSH private key names,credentials). If the path matches, the upload is refused unless you change configuration. - Extra denylist segments — You can add path component names to merge with the built-in list (for example a proprietary secrets directory name).
beforeFileUpload/before_file_uploadhook — Optional hook for each file upload: return a different path, returnfalseto abort, or throw. TypeScript: passbeforeFileUploadon the third argument totools.execute. Python: use@before_file_uploadin themodifierslist (same as other Python modifiers), not a separate keyword. Use this to enforce app-specific policies, audit logging, or copy-on-write to a safe temp file before upload. See Before file upload (Python).- New errors —
ComposioSensitiveFilePathBlockedError/SensitiveFilePathBlockedErrorwhen a path is blocked, andComposioFileUploadAbortedError/FileUploadAbortedErrorwhen the hook returnsfalseor a hook throws in an aborting way. - Python: modifier order —
substitute_file_uploadsruns beforebefore_executemodifiers, matching TypeScript behavior (including Tool Routerexecute_meta).
Examples
Configure the client (defaults + extra denylist segments)
Keep the built-in blocklist enabled and add path component names (anywhere in the resolved path) that your app treats as secret:
import { Composio } from '@composio/core';
const composio = new Composio({
apiKey: process.env.COMPOSIO_API_KEY!,
sensitiveFileUploadProtection: true,
fileUploadPathDenySegments: ['company-secrets', 'private-keys'],
});from composio import Composio
composio = Composio(
api_key="your_composio_key",
sensitive_file_upload_protection=True,
file_upload_path_deny_segments=("company-secrets", "private-keys"),
)Run a hook before each automatic file read
Return a new path, return false / False to abort, or throw. TypeScript passes beforeFileUpload in the third argument to tools.execute. In Python, use @before_file_upload in modifiers:
import { Composio, type beforeFileUploadModifier } from '@composio/core';
import path from 'node:path';
const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY! });
const beforeFileUpload: beforeFileUploadModifier = async ctx => {
// e.g. log, rewrite path, or return false to block
return ctx.path;
};
await composio.tools.execute(
'GOOGLEDRIVE_UPLOAD_FILE',
{
userId: 'user-123',
arguments: { file_to_upload: path.join(__dirname, 'document.pdf') },
dangerouslySkipVersionCheck: true,
},
{ beforeFileUpload },
);import os
from composio import Composio, before_file_upload
@before_file_upload(tools=["GOOGLEDRIVE_UPLOAD_FILE"])
def before_upload(path: str, tool: str, toolkit: str) -> "str | bool":
return path
composio = Composio()
composio.tools.execute(
"GOOGLEDRIVE_UPLOAD_FILE",
{"file_to_upload": os.path.join(os.getcwd(), "document.pdf")},
user_id="user-123",
modifiers=[before_upload],
)Opting out
Set sensitiveFileUploadProtection: false (TypeScript) or sensitive_file_upload_protection=False (Python) only if you have a clear reason and accept the security tradeoff. Prefer copying files to a non-sensitive path or using the hook to gate uploads.
import { Composio } from '@composio/core';
const composio = new Composio({
apiKey: process.env.COMPOSIO_API_KEY!,
sensitiveFileUploadProtection: false,
});from composio import Composio
composio = Composio(
api_key="your_composio_key",
sensitive_file_upload_protection=False,
)Where to read more
- Executing tools: automatic file handling and security options — configuration and code patterns.
- TypeScript
Composioclient — related constructor options in the reference. - TypeScript SDK (repository):
ts/docs/advanced/auto-upload-download.md— extended guide with error types and edge cases.
Webhook Event for Auto-Disabled Triggers
composio.trigger.disabled is a new V3 webhook event that fires when Composio disables one of your triggers without a request from you — expired credentials, a failed webhook-subscription refresh, or unhealthy polling. It's opt-in: add it to a webhook subscription's enabled_events to receive it. Existing subscriptions for composio.trigger.message and composio.connected_account.expired are unaffected.
When it fires
The event is emitted only for platform-initiated disables. data.disabled_reason names the cause:
disabled_reason | What it means |
|---|---|
connection_expired | The connected account entered EXPIRED and the trigger was paused. Auto-re-enabled when the account returns to ACTIVE. |
subscription_auth_failure | The provider rejected the credentials when Composio tried to refresh the webhook subscription. Re-auth the connection to resolve. |
subscription_refresh_failure | Composio could not reach the provider to refresh the webhook subscription (provider error, timeout, or network issue). Re-enable the trigger once the provider is healthy. |
polling_failure_in_composio_infra | Composio's polling service failed to fetch events for this trigger after repeated retries. Rare in practice. |
The event does not fire when you disable a trigger yourself — neither through PATCH /api/v3/trigger_instances/manage/{id} nor by deactivating the connected account via PATCH /api/v3/connected_accounts/{id}/status.
Subscribe
Add composio.trigger.disabled to your subscription's enabled_events — via the dashboard webhook settings or the Webhook Subscriptions API:
curl -X PATCH https://backend.composio.dev/api/v3/webhook_subscriptions/ws_your-subscription-id \
-H "X-API-KEY: <your-composio-api-key>" \
-H "Content-Type: application/json" \
-d '{
"enabled_events": [
"composio.trigger.message",
"composio.trigger.disabled"
]
}'The event is V3-only. Subscriptions on V1 or V2 payloads cannot enable it.
Payload
{
"id": "msg_847cdfcd-d219-4f18-a6dd-91acd42ca94a",
"type": "composio.trigger.disabled",
"metadata": {
"project_id": "pr_your-project-id"
},
"data": {
"id": "ti_your-trigger-id",
"connected_account_id": "ca_your-connected-account-id",
"trigger_name": "GITHUB_COMMIT_EVENT",
"user_id": "your-user-id",
"trigger_config": { "owner": "composio", "repo": "hermes" },
"disabled_at": "2026-04-23T11:59:59.000Z",
"disabled_reason": "connection_expired"
},
"timestamp": "2026-04-23T12:00:00.000Z"
}See subscribing to triggers for handler setup and subscribing to connection expiry events for re-auth flows on connection_expired.