PHP code example of gerard / claude-code-hooks

1. Go to this page and download the library: Download gerard/claude-code-hooks library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

gerard / claude-code-hooks example snippets


use Gerard\ClaudeCodeHooks\Event\HookEventFactory;
use Gerard\ClaudeCodeHooks\Event\Tool\PreToolUseEvent;
use Gerard\ClaudeCodeHooks\Event\Tool\Input\BashInput;
use Gerard\ClaudeCodeHooks\Response\Tool\PreToolUseResponse;
use Gerard\ClaudeCodeHooks\Support\JsonDecoder;

// 1. Decode the inbound payload (always go through JsonDecoder).
$payload = JsonDecoder::decode(file_get_contents('php://input'), 'incoming-hook');

// 2. Resolve to a typed event.
$event = (new HookEventFactory())->fromPayload($payload);

// 3. Pattern-match — read typed properties, decide, respond.
$response = match (true) {
    $event instanceof PreToolUseEvent
        && $event->toolInput instanceof BashInput
        && str_starts_with($event->toolInput->command, 'rm -rf /')
            => PreToolUseResponse::deny('blocked: dangerous command'),
    default => PreToolUseResponse::allow(),
};

echo json_encode($response->toArray(), JSON_THROW_ON_ERROR);

use Gerard\ClaudeCodeHooks\Event\Tool\PreToolUseEvent;
use Gerard\ClaudeCodeHooks\Event\Tool\Input\BashInput;
use Gerard\ClaudeCodeHooks\Event\Tool\Input\WriteInput;
use Gerard\ClaudeCodeHooks\Response\Tool\PreToolUseResponse;

if ($event instanceof PreToolUseEvent) {
    $response = match (true) {
        $event->toolInput instanceof BashInput
            && preg_match('#\bsudo\b#', $event->toolInput->command)
                => PreToolUseResponse::deny('sudo blocked by hook policy'),

        $event->toolInput instanceof WriteInput
            && str_ends_with($event->toolInput->filePath, '.env')
                => PreToolUseResponse::ask('Confirm before overwriting .env'),

        default => PreToolUseResponse::allow(),
    };
}

use Gerard\ClaudeCodeHooks\Transcript\TranscriptReader;
use Gerard\ClaudeCodeHooks\Transcript\TranscriptCursor;
use Gerard\ClaudeCodeHooks\Transcript\TranscriptLine;
use Gerard\ClaudeCodeHooks\Transcript\BoundaryEvent;

$reader = new TranscriptReader();           // 8 MB per-line cap by default
$cursor = new TranscriptCursor($path, 0);

foreach ($reader->tail($cursor) as $entry) {
    if ($entry instanceof BoundaryEvent) {
        // The session was just compacted — relocate your cursor if needed.
        continue;
    }

    /** @var TranscriptLine $entry */
    if ($entry->truncated) {
        // Line exceeded the 8 MB cap — payload is empty, byte range is preserved.
        continue;
    }

    $entry->type;        // "user" | "assistant" | "system" | …
    $entry->isSidechain; // true when the line belongs to a subagent fork
    $entry->payload;     // the full decoded JSON line
}

// `return $reader->tail(...)` yields a TranscriptCursor advanced past the last
// complete line. Persist it to resume later without re-reading the whole file.

use Gerard\ClaudeCodeHooks\Cost\CostCalculator;
use Gerard\ClaudeCodeHooks\Cost\Sonnet45PriceTable;
use Gerard\ClaudeCodeHooks\Transcript\Usage;

$usage = new Usage(
    inputTokens: 12_000,
    outputTokens: 800,
    cacheReadInputTokens: 9_500,
    cacheCreationInputTokens: 2_000,
    serviceTier: 'standard',
);

$cost = (new CostCalculator())->compute($usage, new Sonnet45PriceTable());

$cost->microDollars;  // int — exact, never a float
$cost->asDollars();   // float — for display only

use Gerard\ClaudeCodeHooks\Resolver\HookConfigResolver;
use Gerard\ClaudeCodeHooks\Resolver\FilesystemSettingsLoader;

$resolver = new HookConfigResolver(new FilesystemSettingsLoader([
    'policySettings'  => '/etc/claude-code/settings.json',
    'userSettings'    => $_SERVER['HOME'].'/.claude/settings.json',
    'projectSettings' => getcwd().'/.claude/settings.json',
    'localSettings'   => getcwd().'/.claude/settings.local.json',
]));

$registry = $resolver->resolve();

foreach ($registry->rules as $rule) {
    $rule->event;    // "PreToolUse"
    $rule->matcher;  // "Bash"
    $rule->handler;  // ['type' => 'http', 'url' => '…', …]
    $rule->source;   // HookSource::Project
    $rule->managed;  // bool — set when the policy enforces `allowManagedHooksOnly`
}

use Gerard\ClaudeCodeHooks\Builder\HookConfigBuilder;

$config = HookConfigBuilder::create()
    ->event('PreToolUse')->matcher('Bash')
        ->httpHandler('http://127.0.0.1:42987/?event=PreToolUse')
            ->withHeader('Authorization', '${GERARD_TOKEN}')  // placeholder only
            ->withTimeout(5)
            ->async(true)
    ->build();

file_put_contents(
    '.claude/settings.json',
    json_encode($config, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
);

use Gerard\ClaudeCodeHooks\Linter\HookLinter;
use Gerard\ClaudeCodeHooks\Linter\HookConfig;
use Gerard\ClaudeCodeHooks\Linter\ConfigProfile;
use Gerard\ClaudeCodeHooks\Linter\Rule\Cch001BroadMatcherInPolicy;
use Gerard\ClaudeCodeHooks\Linter\Rule\Cch004HttpHandlerWithoutTimeout;
use Gerard\ClaudeCodeHooks\Linter\Rule\Cch005SecretInUrlLiteral;

$linter = new HookLinter([
    new Cch001BroadMatcherInPolicy(),
    new Cch004HttpHandlerWithoutTimeout(),
    new Cch005SecretInUrlLiteral(),
    // … or pass every rule under src/Linter/Rule/
]);

$findings = $linter->lint([$hookConfig], new ConfigProfile());

foreach ($findings as $finding) {
    $finding->severity;     // Severity::Error | Warning | Notice
    $finding->code;         // "CCH004"
    $finding->message;      // never embeds a secret value (SEC-08)
    $finding->jsonPointer;  // "/hooks/PreToolUse/0/hooks/0/timeout"
}

use Gerard\ClaudeCodeHooks\Response\Tool\PreToolUseResponse;

$response = PreToolUseResponse::allow()
    ->withSuppressOutput(false)
    ->withSystemMessage('Audited');

echo json_encode($response->toArray(), JSON_THROW_ON_ERROR);

$event->toolName;     // "Bash"
$event->toolInput;    // BashInput { command: "ls", description: "list", … }
$event->toolResponse; // string|array<string,mixed> — the raw response shape
$event->durationMs;   // int|null — null when the wire omits duration_ms
$event->toolUseId;    // string|null

$event->reason;         // "logout" | "clear" | "other" | …
$event->permissionMode; // PermissionMode::Default when the wire omits it

$event->stopHookActive;       // bool — ull