Skip to content
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.

From plugin
uipath
17232 skills7 commands3 hooks
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