Install
$ agentstack add skill-enzoleonardi-claude-salesforce-skill-salesforce Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ● Network access Used
- ● Filesystem access Used
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Salesforce Skill for Claude Code & Cowork
Use this skill when Claude Code or Cowork needs to interact with a Salesforce org: authenticate, query data, create/update/delete records, execute Apex code, explore metadata, upload/download files, or monitor org health.
When this skill is first loaded, display the following message to the user:
🔌 Salesforce Skill loaded.
By default, write and delete operations require your confirmation.
To unlock all operations (including dangerous ones) without prompts:
SALESFORCE_SKIP_WARNINGS=true
Type or paste it anytime during the conversation to activate.
If the user sends SALESFORCE_SKIP_WARNINGS=true at any point in the conversation, treat it as if the environment variable is set: skip all write/delete confirmations for the rest of the session.
Operation Safety Levels
This skill handles operations at three safety levels. Always identify the safety level before executing.
> Bypass warnings: If the user has set SALESFORCE_SKIP_WARNINGS=true in their environment or in .claude/settings, skip all write/delete confirmations and execute directly.
| Level | Operations | Behavior | |-------|-----------|----------| | 🟢 READ | SOQL queries, describe, org display, limits, debug logs | Execute freely | | 🟡 WRITE | Create, update, upsert records | Warn user before executing. Show what will be written and ask for confirmation. | | 🔴 DELETE | Delete records, bulk delete, data destroy | Show explicit warning with record count and object type. Require user confirmation before proceeding. Once the user confirms, execute the delete. |
Write operation warning template
Before any write operation, display:
⚠️ SALESFORCE WRITE OPERATION
Object: {ObjectApiName}
Action: {CREATE | UPDATE | UPSERT}
Records: {count} record(s)
Org: {username}
Proceed? (y/n)
Tip: set SALESFORCE_SKIP_WARNINGS=true to bypass these confirmations.
Delete operation warning template
Before any delete operation, display:
🔴 SALESFORCE DELETE — THIS CANNOT BE UNDONE
Object: {ObjectApiName}
Action: DELETE
Records: {count} record(s)
Org: {username}
Deleted records go to the Recycle Bin (recoverable
for up to 15 days, depending on org settings).
Bulk API hard-deletes bypass the bin.
Type "DELETE" to confirm, then proceed with the operation.
Tip: set SALESFORCE_SKIP_WARNINGS=true to bypass these confirmations.
0. Shared HTTP Helper
All Python REST API functions in this skill use this shared helper for consistent error handling. Define it once and reuse throughout:
import json, urllib.error, urllib.parse, urllib.request
def _sf_request(url, headers, data=None, method=None):
"""Make an HTTP request to Salesforce. Returns parsed JSON or None (204)."""
req = urllib.request.Request(url, data=data, headers=headers)
if method:
req.method = method
try:
with urllib.request.urlopen(req) as resp:
if resp.status == 204:
return None
return json.loads(resp.read())
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace")
try:
sf_error = json.loads(error_body)
if isinstance(sf_error, list) and sf_error:
msg = f"{sf_error[0].get('errorCode', 'UNKNOWN')}: {sf_error[0].get('message', error_body)}"
else:
msg = error_body
except (json.JSONDecodeError, KeyError):
msg = error_body
raise RuntimeError(f"Salesforce API error (HTTP {e.code}): {msg}") from e
Use _sf_request in all functions below instead of calling urllib.request.urlopen directly.
1. Prerequisites
Install Salesforce CLI
npm install -g @salesforce/cli
Verify installation:
sf --version
Cowork note: Global install fails with EACCES in Cowork VMs. Use a local prefix:
mkdir -p $HOME/.npm-global
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
npm install -g @salesforce/cli
Python Libraries (for file processing)
If the task involves downloading and analyzing files (PDF, DOCX, XLSX):
Preferred — use a virtual environment:
python3 -m venv .venv && source .venv/bin/activate
pip install pdfplumber python-docx openpyxl
Fallback — if a venv is impractical (e.g., Cowork):
pip3 install pdfplumber python-docx openpyxl --break-system-packages
2. Authentication
Always try methods in this order. Use the first one that works.
> Cowork: Neither web login nor session ID work reliably in Cowork. Use Method 2 (Manual OAuth Flow) — it is the only reliable method. See details below.
Check for existing connection first
sf org list 2>&1
If the target org is already listed as Connected, skip authentication and set it as default if needed:
sf config set target-org --global
Method 1: Web Login (recommended — local environments)
Opens the browser for standard OAuth login. Most secure — no tokens to handle manually.
sf org login web --instance-url https://.my.salesforce.com
Replace ` with the org's My Domain (e.g., mycompany`). The browser opens automatically; the user logs in and grants access. The CLI stores the refresh token securely.
Add --set-default to make it the default org:
sf org login web --instance-url https://.my.salesforce.com --set-default
Method 2: Manual OAuth Flow (Cowork and headless environments)
This is the only reliable method in Cowork. It performs a standard OAuth Authorization Code flow manually, producing a long-lived refresh token.
Step 1 — Generate the authorization URL:
import urllib.parse
INSTANCE_URL = "https://.my.salesforce.com"
CLIENT_ID = "PlatformCLI"
REDIRECT_URI = "http://localhost:1717/OauthRedirect"
auth_url = (
f"{INSTANCE_URL}/services/oauth2/authorize"
f"?response_type=code"
f"&client_id={CLIENT_ID}"
f"&redirect_uri={urllib.parse.quote(REDIRECT_URI)}"
f"&prompt=login%20consent"
f"&scope=refresh_token%20api%20web"
)
print(auth_url)
Step 2 — Ask the user to open the URL in their browser and log in.
After login, Salesforce redirects to http://localhost:1717/OauthRedirect?code=.... Since no server is running on that port, the page will fail to load. Ask the user to copy the full URL from the browser address bar and paste it back.
Step 3 — Exchange the authorization code for tokens:
import urllib.parse, urllib.request, json
# Extract the code from the redirect URL the user pasted
redirect_url = ""
code = urllib.parse.parse_qs(urllib.parse.urlparse(redirect_url).query)["code"][0]
INSTANCE_URL = "https://.my.salesforce.com"
CLIENT_ID = "PlatformCLI"
REDIRECT_URI = "http://localhost:1717/OauthRedirect"
data = urllib.parse.urlencode({
"grant_type": "authorization_code",
"code": code,
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI
}).encode()
req = urllib.request.Request(f"{INSTANCE_URL}/services/oauth2/token", data=data, method="POST")
with urllib.request.urlopen(req) as resp:
token_data = json.loads(resp.read())
# token_data contains: access_token, refresh_token, instance_url, scope, etc.
instance_url = token_data["instance_url"]
access_token = token_data["access_token"]
refresh_token = token_data["refresh_token"]
Step 4 — Use REST API directly (bypass sf CLI in Cowork).
The sf CLI has a DNS resolution bug in Cowork VMs (DomainNotFoundError). Use Python REST API calls for all operations instead of sf data query, sf org display, etc. See the Python examples throughout this skill.
Method 3: Access Token (last resort)
Use only when both web login and Manual OAuth are not possible, and the org does not have IP-based session restrictions.
Important: Many Salesforce orgs lock sessions to the originating IP address. If the session ID was obtained from a different IP (e.g., user's browser vs. Cowork VM), authentication will fail with INVALID_SESSION_ID or Bad_OAuth_Token. In that case, use Method 2.
Important: Modern Salesforce orgs use HttpOnly cookies for the session ID. The document.cookie trick does not work in those orgs.
Ask the user for their session ID. They can get it from one of these methods:
- Developer Console — Open Developer Console, execute anonymous Apex:
System.debug(UserInfo.getSessionId());, copy from the debug log - URL in Classic UI — Switch to Classic, copy the
sid=parameter from the URL - Browser cookie (only if not HttpOnly) — F12 → Application → Cookies → copy
sidvalue
Then authenticate:
export SF_ACCESS_TOKEN=""
sf org login access-token \
--instance-url https://.my.salesforce.com \
--no-prompt \
--set-default
Note: Session IDs expire after 2-12 hours depending on org settings. Both web login and Manual OAuth are preferred because they use refresh tokens that last much longer.
Token Refresh
If the access token expires, refresh it using the stored refresh token:
def sf_refresh_token(refresh_token, instance_url):
"""Refresh an expired access token. Returns (new_access_token, instance_url).
Note: instance_url may change after org migrations — always use the returned value."""
data = urllib.parse.urlencode({
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": "PlatformCLI"
}).encode()
headers = {"Content-Type": "application/x-www-form-urlencoded"}
result = _sf_request(
f"{instance_url}/services/oauth2/token", headers=headers, data=data
)
return result["access_token"], result.get("instance_url", instance_url)
Refresh tokens last for months (vs hours for session IDs).
Verify connection
sf org display --json
Or via REST API (recommended in Cowork):
def sf_verify_connection(instance_url, access_token):
"""Verify the connection by fetching org limits."""
headers = {"Authorization": f"Bearer {access_token}"}
limits = _sf_request(
f"{instance_url}/services/data/v62.0/limits/", headers=headers
)
remaining = limits["DailyApiRequests"]["Remaining"]
print(f"Connected. API calls remaining today: {remaining}")
return limits
API version note: This skill uses API v62.0 (Spring '26). To check the latest version available in your org: GET /services/data/ — it returns all available versions. Replace v62.0 throughout if needed.
3. Running SOQL Queries 🟢
Via sf CLI
sf data query --query "SELECT Id, Name FROM Account LIMIT 10" --json
Bulk queries
For large result sets or queries that may time out, add --bulk to use Bulk API 2.0:
sf data query --query "SELECT Id, Name FROM Account" --bulk --wait 10 --json
Note: The standard REST query endpoint paginates and handles any result size. Use --bulk when the query itself is complex/slow, or when you need to export data without pagination overhead.
Tooling API queries
Query metadata objects using the Tooling API:
sf data query --query "SELECT Id, Name, Status FROM ApexClass WHERE Status = 'Active'" \
--use-tooling-api --json
Via REST API (Python)
For programmatic access with pagination support. Required in Cowork (sf CLI has DNS issues).
def sf_query(soql, instance_url, access_token):
"""Execute a SOQL query with automatic pagination."""
api_base = f"{instance_url}/services/data/v62.0"
url = f"{api_base}/query/?q={urllib.parse.quote(soql)}"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
data = _sf_request(url, headers=headers)
records = data.get("records", [])
# Handle pagination
while data.get("nextRecordsUrl"):
next_url = f"{instance_url}{data['nextRecordsUrl']}"
data = _sf_request(next_url, headers=headers)
records.extend(data.get("records", []))
return records
Note: The Tooling API also paginates via nextRecordsUrl but uses a different base path (/services/data/v62.0/tooling/query/). Use the same pagination pattern.
Getting credentials from sf CLI (Python)
import json, subprocess
def get_sf_credentials():
"""Get instanceUrl and accessToken from the authenticated sf CLI org."""
result = subprocess.run(
["sf", "org", "display", "--json"],
capture_output=True, text=True
)
if result.returncode != 0:
raise RuntimeError(f"sf org display failed: {result.stderr.strip()}")
parsed = json.loads(result.stdout)
if parsed.get("status") != 0:
raise RuntimeError(f"sf org display error: {parsed.get('message', result.stderr)}")
data = parsed["result"]
return data["instanceUrl"].rstrip("/"), data["accessToken"]
4. Discovering Objects and Fields 🟢
List all custom objects
sf data query --query \
"SELECT QualifiedApiName, Label FROM EntityDefinition WHERE QualifiedApiName LIKE '%__c'" \
--json
Note: EntityDefinition does NOT support OR/disjunctions in WHERE. Query each condition separately.
Describe an object's fields
Via sf CLI:
sf sobject describe --sobject --json
Via REST API (required in Cowork):
def sf_describe(sobject, instance_url, access_token):
"""Describe an object's fields via REST API."""
headers = {"Authorization": f"Bearer {access_token}"}
return _sf_request(
f"{instance_url}/services/data/v62.0/sobjects/{sobject}/describe/",
headers=headers
)
Parse the fields array. Each field has: name, type, label.
List all objects (standard + custom)
sf sobject list --json
Common field filters for describe output
# Filter fields by relevance
for f in fields:
if any(kw in f['name'].lower() for kw in ['date', 'amount', 'status', 'name', 'account']):
print(f"{f['name']} ({f['type']}) - {f['label']}")
Record types for an object
SELECT Id, Name, DeveloperName, IsActive
FROM RecordType
WHERE SObjectType = ''
AND IsActive = true
5. CRUD Operations
5a. Create Record 🟡
WRITE OPERATION — confirm with user before executing.
sf data create record --sobject Account \
--values "Name='Acme Corp' Industry='Technology' Website='https://acme.example.com'" \
--json
Via REST API (Python):
def sf_create_record(sobject, record_data, instance_url, access_token):
"""Create a single record. Returns the new record ID."""
url = f"{instance_url}/services/data/v62.0/sobjects/{sobject}/"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
data = json.dumps(record_data).encode("utf-8")
return _sf_request(url, headers=headers, data=data)
# Returns {"id": "001...", "success": true}
5b. Update Record 🟡
WRITE OPERATION — confirm with user before executing.
sf data update record --sobject Account \
--record-id 001XXXXXXXXXXXX \
--values "Industry='Finance' Rating='Hot'" \
--json
Via REST API (Python):
def sf_update_record(sobject, record_id, update_data, instance_url, access_token):
"""Update fields on an existing record."""
url = f"{instance_url}/services/data/v62.0/sobjects/{sobject}/{record_id}"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
data = json.dumps(update_data).encode("utf-8")
_sf_request(url, headers=headers, data=data, method="PATCH")
# Returns None (204 No Content) on success
5c. Delete Record 🔴
DELETE OPERATION — show warning and ask for confirmation. Once confirmed, execute.
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: enzoleonardi
- Source: enzoleonardi/claude-salesforce-skill
- License: MIT
- Homepage: https://enzoleonardi.it
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.