AI & Agents
Hook
Hooks
What uipath runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add uipath/skills > /plugin install uipath@uipath-marketplace
Ships with uipath. Installing the plugin gets these hooks.
Where it lives
- hooks/set-session-env.mjsGitHub
Read the script
#!/usr/bin/env node // SessionStart step: export the agent's session context to the uip CLI. // // Reads the SessionStart payload on stdin and appends `export` lines to // $CLAUDE_ENV_FILE so every subsequent shell tool subprocess — and therefore // every `uip` command the agent runs — inherits them: // // UIPATH_SESSION_ID top-level `session_id`. The CLI puts it on App // Insights' native `ai.session.id` tag (query it as // `session_Id`) for every command it runs, including // the `uip track --hook` ingestions hooks.json // registers (UiPath/cli#3431) — so the command stream // and the skills events share one session id. This // export is the ONLY way they correlate: since schema // v3 the telemetry hook sends no session id of its // own. // UIPATH_AGENT_MODEL top-level `model` (string, or `{id, display_name}`) // UIPATH_PERMISSION_MODE top-level `permission_mode` // UIPATH_EFFORT_LEVEL `effort.level` // UIPATH_SKILLS_VERSION `skillsVersion` from this plugin's // version-manifest.json // // The CLI stamps the last four on every command request as the optional // `agent_model` / `permissionMode` / `effortLevel` / `skillsVersion` // dimensions — the same keys the skills events carry. They are the // session-start snapshot: SessionStart re-fires on resume/clear/compact and // re-exports any value that changed (the last export wins when sourced). // // Registered SYNCHRONOUSLY in hooks.json (no "async": true): the write must // complete before the session's first shell call, or early `uip` commands // would miss the values. Costs a few ms (one small JSON parse, no network, no // uip call). // // Deliberately NOT gated on UIPATH_TELEMETRY_DISABLED: writing a variable // transmits nothing — whether any event carrying it is ever sent stays // governed by the CLI's own telemetry gate. // // Safety: // - host wins: no UIPATH_SESSION_ID export when it is already set in the env; // - idempotent: the id is appended only when the env file exports none yet; // a context variable only when the file does not already export that // exact value; // - injection-safe: $CLAUDE_ENV_FILE is sourced by the agent, so every value // is stripped to a quote-free charset and length-capped before being // written inside single quotes (agent session ids are UUIDs, so a // legitimate id is never altered); // - never-fail: always exits 0, never blocks the session. // // Runs under Node.js (>= 20); hooks.json guards the spawn and silently no-ops // when `node` is not on PATH. import fs from "node:fs"; const MANIFEST_URL = new URL("../version-manifest.json", import.meta.url); function isPlainObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function str(value) { return typeof value === "string" ? value : ""; } // Same bound the CLI applies to every agent-supplied dimension. function sanitizeToken(value) { return value.replace(/[^A-Za-z0-9:._/ -]/g, "_").slice(0, 120); } function sanitizeSessionId(value) { return value.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 64); } function readSkillsVersion() { try { return str(JSON.parse(fs.readFileSync(MANIFEST_URL, "utf8")).skillsVersion); } catch { return ""; } } function agentModel(payload) { const model = payload.model; if (isPlainObject(model)) return str(model.id) || str(model.display_name); return str(model); } /** The value the env file currently exports for `name` (the last export * wins when the file is sourced), or undefined when it exports none. */ function exportedValue(existing, name) { let value; for (const match of existing.matchAll(new RegExp(`^export ${name}='([^']*)'\\r?$`, "gm"))) { value = match[1]; } return value; } function main() { const envFile = process.env.CLAUDE_ENV_FILE; if (!envFile) return; let payload; try { payload = JSON.parse(fs.readFileSync(0, "utf8")); } catch { return; } if (!isPlainObject(payload)) return; let existing = ""; try { existing = fs.readFileSync(envFile, "utf8"); } catch { existing = ""; } const lines = []; const sid = sanitizeSessionId(str(payload.session_id)); if (sid && !process.env.UIPATH_SESSION_ID && !/^export UIPATH_SESSION_ID=/m.test(existing)) { lines.push(`export UIPATH_SESSION_ID='${sid}'`); } const effort = isPlainObject(payload.effort) ? payload.effort : {}; const context = [ ["UIPATH_AGENT_MODEL", agentModel(payload)], ["UIPATH_PERMISSION_MODE", str(payload.permission_mode)], ["UIPATH_EFFORT_LEVEL", str(effort.level)], ["UIPATH_SKILLS_VERSION", readSkillsVersion()], ]; for (const [name, raw] of context) { const value = sanitizeToken(raw.trim()); if (value && exportedValue(existing, name) !== value) { lines.push(`export ${name}='${value}'`); } } if (lines.length === 0) return; // If the file exists but doesn't end with a newline (another hook's partial // write), appending directly would concatenate onto its last line and could // break the sourced env file for the whole session — repair it first. const prefix = existing && !existing.endsWith("\n") ? "\n" : ""; try { fs.appendFileSync(envFile, `${prefix}${lines.join("\n")}\n`); } catch { // never-fail } } try { main(); } catch { // never-fail: always exit 0, never block the session } process.exit(0); - hooks/suggest-permissions.mjsGitHub
Read the script
#!/usr/bin/env node // Detects whether Claude Code has an allowlist for `uip` subcommands. // If none is found, prints a one-line nudge pointing at /uipath:install-permissions. // Non-blocking — never fails the session, even if detection fails. // // Runs under Node.js (>= 20), which is present wherever the skills were // installed through the Node-based `uip` CLI. hooks.json guards the spawn and // silently no-ops when `node` is not on PATH. import fs from "node:fs"; import os from "node:os"; import path from "node:path"; function main() { // Only run inside a Claude Code plugin context. if (!process.env.CLAUDE_PLUGIN_ROOT) return; // Codex exposes Claude-compatible plugin environment variables for hook // compatibility. This nudge is Claude-specific, so keep Codex sessions silent. if (process.env.PLUGIN_ROOT) return; // Candidate settings files, most-to-least specific. const candidates = []; if (process.env.CLAUDE_PROJECT_DIR) { candidates.push(path.join(process.env.CLAUDE_PROJECT_DIR, ".claude/settings.local.json")); candidates.push(path.join(process.env.CLAUDE_PROJECT_DIR, ".claude/settings.json")); } candidates.push(path.join(process.cwd(), ".claude/settings.local.json")); candidates.push(path.join(process.cwd(), ".claude/settings.json")); candidates.push(path.join(os.homedir(), ".claude/settings.json")); // If any candidate already mentions Bash(uip...) in its permissions, stay silent. // Intentional simplification: this matches `allow`, `ask`, AND `deny` blocks. // Any explicit `uip` rule means the user has made a decision about this CLI — // we don't second-guess by nudging them toward a permissive allowlist. for (const file of candidates) { let content = ""; try { content = fs.readFileSync(file, "utf8"); } catch { continue; } if (content.includes("Bash(uip")) return; } // No allowlist detected — print a one-line nudge to stderr (the Claude Code // SessionStart convention for status messages). process.stderr.write( "uipath: To skip 25+ approval prompts per uip build, run: /uipath:install-permissions\n", ); } try { main(); } catch { // never-fail: detection problems must not block the session } process.exit(0); - hooks/validate-skill-descriptions.shGitHub
Read the script
#!/bin/bash # Validate skill description lengths # Enforces a 1024-character limit on all SKILL.md descriptions. # Claude Code truncates the combined `description` + `when_to_use` at 1,536 chars # in the skill listing (https://code.claude.com/docs/en/skills.md). 1024 keeps # us comfortably under that cap while leaving headroom for `when_to_use`. # # Usage: # validate-skill-descriptions.sh [file1 file2 ...] # If no files specified, checks staged files (for pre-commit hook) set -e LIMIT=1024 FAILED=0 # Determine which files to check if [ "$#" -eq 0 ]; then # Pre-commit mode: check staged SKILL.md files FILES=$(git diff --cached --name-only --diff-filter=ACM | grep 'skills/.*/SKILL\.md$' || true) else # Explicit mode: use provided files FILES="$@" fi for file in $FILES; do if [ ! -f "$file" ]; then continue fi # Extract description from frontmatter desc=$(sed -n 's/^description: "\(.*\)"$/\1/p' "$file" | head -1) # Also handle descriptions without surrounding quotes if [ -z "$desc" ]; then desc=$(sed -n 's/^description: \(.*\)$/\1/p' "$file" | head -1) fi len=${#desc} if [ "$len" -gt "$LIMIT" ]; then echo "❌ $file: description exceeds $LIMIT characters ($len chars)" FAILED=1 else echo "✓ $file: $len chars" fi done if [ "$FAILED" -eq 1 ]; then echo "" echo "Skill description validation failed. Descriptions must be ≤ $LIMIT characters." echo "Claude Code truncates description + when_to_use at 1,536 chars in the skill listing;" echo "this repo caps at $LIMIT to leave headroom and keep descriptions focused." echo "Edit the 'description' field in SKILL.md frontmatter and try again." exit 1 fi exit 0
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withuipath
UiPath Agent Skills give AI coding agents the domain knowledge to build, run, test, and deploy UiPath automations and agents — directly from your development environment.
Get the whole plugin
Stats
172
Stars
58
Forks
Active
Maintenance
Python
Language
MIT
License
1h ago
Last commit
9mo ago
Created
4h ago
Added

