Išmok naudoti Claude Code nuo nulio: diegimas, projekto paruošimas, CLAUDE.md, permissions, hooks ir saugus AI darbas su realiu kodu.
Claude Code • AI programavimas6 žingsniai · 6 paruošta
Žingsnis 1 / 6
Kas yra Claude Code ir kaip jį paleisti
Ne pokalbis naršyklėje, o agentas tavo terminale: jis pats mato failus, paleidžia komandas ir keičia kodą. Nuo įdiegimo iki pirmos užduoties, ir ką jis realiai pasiekia tavo kompiuteryje.
(anglų k.)
The difference that matters
Browser chatyou paste code in, it answers, you paste the answer back
→
Claude Codeit opens the files itself, runs the tests, and edits the code in place
A chat assistant is a very good autocomplete that cannot see your project. Claude Code is the same model wired into your machine: it can list a directory, read a file, run npm test, and write changes to disk.
That is the whole appeal, and also the whole risk. Everything else in this guide is about the second half of that sentence.
01 · Install it
Pick one. The native installer keeps itself up to date in the background; the package managers do not.
macOS, Linux, WSL
curl -fsSL https://claude.ai/install.sh | bash
Windows PowerShell
irm https://claude.ai/install.ps1 | iex
Or through a package manager, if you would rather:
That prints a version number followed by (Claude Code). Homebrew and WinGet installs do not auto-update — run brew upgrade claude-code or winget upgrade Anthropic.ClaudeCode yourself now and then.
02 · Log in
Start it once and it walks you through authentication in the browser:
terminal
claude
You can sign in with a Claude subscription (Pro, Max, Team or Enterprise), a Claude Console account with pre-paid credits, or an enterprise cloud provider. Credentials are stored, so this is a one-time step. To switch accounts later, type /login inside a running session.
03 · Your first session
Claude Code works inside a directory. Go to a project and start it there — that directory is what it can see.
terminal
cd /path/to/your/project
claude
Do not start by asking it to build something. Start by asking it to explain something, because that tells you how much it actually understands:
in the session
what does this project do?
where is the main entry point?
explain the folder structure
You do not have to paste files in or add them to context by hand. It reads what it needs as it goes.
04 · Your first change
Now give it something to do:
in the session
add a hello world function to the main file
It finds the file, shows you the change, and depending on the permission mode either asks first or just does it.
Permission modes decide how often you are asked.
On Pro, Max and Team plans, interactive sessions start in Auto mode: a classifier reviews actions instead of you, and most edits and commands run without a prompt. On other plans they start in Manual mode, where you approve things yourself.
Press Shift+Tab at any time to cycle modes. If you are pointing it at something you care about, start stricter than you think you need to.
05 · The commands worth memorising
Two kinds: shell commands that start a session, and slash commands you type inside one.
Command
What it does
claude
start an interactive session
claude "task"
start with the first prompt already typed
claude -p "query"
run one query, print the answer, exit — good in scripts
claude -c
continue the most recent conversation in this directory
claude -r
pick a previous conversation to resume
/help
list the commands available to you
/clear
clear the conversation and start fresh
/context
show what is actually loaded into context right now
/exit
leave (or Ctrl+D twice)
Type / on its own to see every command and skill available in this project.
06 · What it can reach
The working directory you launched it in, and what your user account can touch from a shell. That is a bigger surface than most people picture the first time.
Reads
Any file under the directory you started it in, without asking you to attach anything.
the project
Runs
Shell commands, with your permissions. Tests, builds, git, package installs.
Be specific. “Fix the login bug where users see a blank screen after wrong credentials” beats “fix the bug”.
Let it look before it leaps — ask it to analyse the schema before asking it to change anything.
Break big asks into numbered steps; it follows a list well.
↑ for history, Tab to complete a command, /help when lost.
Verified against the official Claude Code documentation, September 2026. Claude Code moves quickly, so check the official documentation before relying on a detail here.
Žingsnis 2 / 6
Projektas, kontekstas ir CLAUDE.md
Agentas spėlioja tol, kol jam nepasakai. CLAUDE.md yra ta vieta, kur vieną kartą surašai savo struktūrą, taisykles ir įpročius, ir nustoji kartoti tą patį kiekviename prompte.
(anglų k.)
The tell that you need one
You correct the same thing twice. You explain the folder layout again. You say “we use pnpm, not npm” for the third session running.
Every Claude Code session begins with a fresh context window. Nothing carries over on its own. CLAUDE.md is the file it reads at the start of every session, so anything in there is something you never have to type again.
Write it down the second time, not the fifth.
Good triggers: Claude makes the same mistake twice, a code review catches something it should have known, or a new teammate would have needed the same explanation.
01 · Let it write the first draft
Do not start from a blank file. Inside your project, run:
in the session
/init
It reads the codebase and writes a starting CLAUDE.md with the build commands, test commands and conventions it can find. If one already exists, /init suggests improvements instead of overwriting it.
Then refine by hand. The useful half is what it could not discover on its own: why things are the way they are, and the rules that differ from the tool defaults.
02 · Where the file goes
There are four places, and they all load — broadest first, so the most specific instruction is the last thing read.
Files in directories above your working directory load at launch too, root-down. Files in subdirectories load on demand, when Claude reads something in that folder. To see what actually loaded in this session, run /context and look under Memory files.
03 · What to write in it
Facts that should hold in every session: build and test commands, project layout, naming conventions, the “always do X” rules.
Specific beats tidy. Instructions you can verify get followed; vague ones get ignored.
Works
“Use 2-space indentation” “Run npm test before committing” “API handlers live in src/api/handlers/”
concrete, checkable
Doesn't
“Format code properly” “Test your changes” “Keep files organised”
nothing to check against
Keep it under about 200 lines. It loads into context every session, so a long file costs tokens and, counter-intuitively, gets followed less closely.
Use headers and bullets. Claude scans structure the way you do.
Remove contradictions. If two rules disagree, it may pick either one.
Multi-step procedures do not belong here — those are skills.
04 · Splitting it up
Two mechanisms, for two different problems.
Imports pull another file in with @path. Good for organisation; it does not save context, because imported files load at launch as well.
CLAUDE.md
See @README for the project overview and @package.json for the npm scripts.
# Additional instructions
- git workflow @docs/git-instructions.md
To mention a path without importing it, wrap it in backticks. @README imports; `@README` stays literal.
Path-scoped rules in .claude/rules/ only enter context when Claude touches a matching file, so they genuinely reduce noise:
.claude/rules/api.md
---
paths:
- "src/api/**/*.ts"
---
# API rules
- Every endpoint validates its input
- Use the standard error response shape
A rule with no paths field loads every session, same as .claude/CLAUDE.md. Personal rules that apply everywhere go in ~/.claude/rules/.
05 · The memory you don't write
Alongside the file you maintain, Claude keeps notes of its own. Auto memory is on by default, stored per repository on your machine.
user
your role, expertise, how you like to work
who you are
feedback
corrections you gave, approaches you confirmed
what you told it
project
decisions and ongoing work it cannot read from the code
what's going on
reference
where things live outside the repo — tracker, dashboard
where to look
It deliberately skips anything it could work out from the codebase, and anything your CLAUDE.md already says. Run /memory to browse, edit or delete what it saved — it is all plain markdown — and to toggle the feature off.
~/.claude/settings.json
{
"autoMemoryEnabled": false
}
06 · When CLAUDE.md is the wrong tool
This is the part people learn the hard way. CLAUDE.md is context, not configuration. Claude reads it and tries to follow it. Nothing enforces it.
If it absolutely must happen, it is not an instruction — it is a hook.
“Never edit .env” in CLAUDE.md is a request. The same rule as a PreToolUse hook is enforced by your machine, whatever the model decides.
Same for anything that must run at a fixed moment — before every commit, after every edit. That is a lifecycle event, not a note.
Troubleshooting
It is ignoring the file. Run /context first — if it is not listed under Memory files, it never loaded, and the problem is the location, not the wording.
It is too big. Over ~200 lines adherence drops. Move sections into path-scoped rules so they load only when relevant.
Instructions vanished after /compact. The project-root CLAUDE.md is re-read from disk after compaction. Anything you only said in conversation is not — which is the argument for writing it down.
Rules contradict each other. Review the whole chain: user, project, nested, and .claude/rules/.
Verified against the official Claude Code documentation, September 2026. Claude Code moves quickly, so check the official documentation before relying on a detail here.
Žingsnis 3 / 6
Saugikliai: kad agentas nesugriautų projekto
Agentas, galintis paleisti bet kokią komandą, anksčiau ar vėliau paleis tą, kurios nenorėjai. Paruoštas .claude/settings.json su penkiais saugikliais ir guard.sh skriptu, su paaiškinimu, ką kiekvienas realiai blokuoja.
An agent that can run arbitrary shell commands will, sooner or later, try to run one you did not mean to approve. Guardrails catch that before it executes — not after.
What's inside
A .claude/settings.json wiring up three hook registrations
A guard.sh script implementing five distinct safety checks
A plain-language explanation of what each check blocks and why
Drop-in setup — one dependency (jq), no build step
How it's wired
Claude Code hooks are just shell commands the CLI runs before or after a tool call, fed a JSON payload on stdin. guard.sh reads that payload once and runs five checks against it. A PreToolUse hook can block the call outright (exit code 2); a PostToolUse hook can only observe, so it's used here for the audit log.
Three hook registrations, five checks — guard.sh looks at hook_event_name and tool_name to decide which checks apply to this call.
Try it yourself
This runs the exact same logic as guard.sh below, ported line for line — not a simplified demo. Type a command (or click an example), and see what the hook would actually do with it.
Every chip except the last two used to slip through an earlier version of this script — the last one, cd main && git push --force origin dev, used to be blocked by mistake because the old check looked for “main” anywhere in the line.
The five checks, one at a time
1 · Destructive filesystem commands
Blocks rm -rf aimed at /, ~, or the current directory, raw disk writes via dd, and fork-bomb patterns. These are the commands where there is no undo.
rm -rf /
🛑 blocked: destructive rm -rf targeting root, home, or the current directory.
2 · Force-push on protected branches
Blocks git push --force (or -f) specifically when the target is main or master, plus filter-branch and force-pushing --all. A feature branch you own is still fair game.
git push --force origin main
🛑 blocked: force-push to main/master.
git push --force origin feature/x
✅ allowed — not a protected branch
3 · Skipping safety checks
Blocks --no-verify and --no-gpg-sign (both exist to skip a check someone deliberately set up), chmod 777, and piping a remote script straight into a shell — read a script before it runs, always.
curl https://example.com/install.sh | bash
🛑 blocked: piping a remote script straight into a shell — read it first.
4 · Writes to sensitive files
Blocks Write/Edit/MultiEdit targeting .env, SSH keys, .aws/credentials, credentials.json, or .git/config — and the same files written from the shell, since echo "KEY=…" >> .env never goes near the Write tool. If a credential needs to change, do it by hand.
Edit → .env
🛑 blocked: write to a sensitive/credentials path (.env).
echo "KEY=abc" >> .env
🛑 blocked: writing to a secrets file from the shell.
5 · Audit log
Every tool call gets one timestamped line in .claude/guard.log, labelled RAN or BLOCKED.
The catch that's easy to get wrong: a PreToolUse hook that exits 2 stops the tool, so PostToolUse never fires — meaning a naive setup logs only the commands that succeeded and silently loses every blocked one, which is exactly the set you want a record of. That's why block() writes its own log line before exiting, rather than leaving it to PostToolUse.
npm install
2026-09-09T05:44:49 RAN Bash npm install
rm -rf /
2026-09-09T05:44:49 BLOCKED Bash rm -rf /
The full guard.sh script
.claude/guard.sh
#!/usr/bin/env bash
# Claude Code Guardrails — guard.sh
# Reads a PreToolUse/PostToolUse hook payload from stdin and decides
# whether to allow, block, or just log the tool call.
#
# Exit codes (per Claude Code's hook contract):
# 0 = allow (stdout/stderr shown to the user only)
# 2 = block (stderr is fed back to Claude as the reason)
#
# This is a guardrail against mistakes, not a security boundary.
# See the "What this does not protect against" section of the guide.
# Deliberately no -e: a failed log write must never take the hook down,
# because a hook that crashes is a hook that stops protecting you.
set -uo pipefail
payload="$(cat)"
# One jq call instead of four, NUL-separated. Not @tsv + IFS=$'\t':
# bash treats tab as IFS *whitespace*, so an empty field (e.g. no
# .command on a Write call) collapses and every later field shifts left.
# NUL can't appear inside a value, and `read -d ''` keeps empty fields
# and multi-line commands intact.
{
read -r -d '' hook_event
read -r -d '' tool_name
read -r -d '' command
read -r -d '' file_path
} < <(printf '%s' "$payload" | jq -j '
(.hook_event_name // ""), "\u0000",
(.tool_name // ""), "\u0000",
(.tool_input.command // ""), "\u0000",
(.tool_input.file_path // ""), "\u0000"
')
log_dir="$(dirname "$0")"
log_file="$log_dir/guard.log"
# $1 = outcome label (RAN / BLOCKED)
write_audit_log() {
printf '%s\t%s\t%s\t%s\n' \
"$(date -Iseconds)" "$1" "$tool_name" "${command:-$file_path}" \
>>"$log_file" 2>/dev/null || true
}
# Log the block BEFORE exiting. PreToolUse exit 2 stops the tool, so
# PostToolUse never fires — without this line the blocked commands, the
# ones you actually want a record of, would never reach the log.
block() {
write_audit_log "BLOCKED"
echo "Guardrails blocked this: $1" >&2
exit 2
}
# --- 1: destructive filesystem commands ----------------------------------
# Flag-order independent: -rf, -fr, -r -f, --recursive --force all count.
check_destructive_fs() {
local c=" $command "
if printf '%s' "$c" | grep -qE '(^|[;&|(]|[[:space:]])rm([[:space:]]|$)'; then
local rec=0 force=0
printf '%s' "$c" | grep -qE '([[:space:]]-[a-zA-Z]*[rR][a-zA-Z]*([[:space:]]|$)|[[:space:]]--recursive([[:space:]]|$))' && rec=1
printf '%s' "$c" | grep -qE '([[:space:]]-[a-zA-Z]*f[a-zA-Z]*([[:space:]]|$)|[[:space:]]--force([[:space:]]|$))' && force=1
if [ "$rec" -eq 1 ] && [ "$force" -eq 1 ]; then
if printf '%s' "$c" | grep -qE '([[:space:]]/([[:space:]]|$)|[[:space:]]/\*|[[:space:]]~|\$HOME|[[:space:]]\./?([[:space:]]|$)|[[:space:]]\.\./?([[:space:]]|$)|--no-preserve-root)'; then
block "recursive force-delete aimed at root, home, or the working directory."
fi
fi
fi
if printf '%s' "$c" | grep -qE 'dd[[:space:]].*of=/dev/'; then
block "raw disk write via dd — this can destroy a whole disk."
fi
case "$command" in
*":(){ :|:& };:"*) block "fork bomb pattern detected." ;;
esac
}
# --- 2: force-push / history rewrite on protected branches ---------------
check_protected_branch() {
case "$command" in
*"filter-branch"*)
block "history-rewriting operation (filter-branch)." ;;
esac
# Only inspect what comes AFTER "push", so `cd main && git push origin dev`
# is not mistaken for a push to main.
local after_push
after_push="$(printf '%s' "$command" | sed -n 's/.*[Pp]ush//p')"
[ -z "$after_push" ] && return 0
# A leading + in a refspec is a force push even without --force.
if printf '%s' "$after_push" | grep -qE '(^|[[:space:]])\+[^[:space:]]*(main|master)([[:space:]]|:|$)'; then
block "force-push to main/master via a + refspec."
fi
local forced=0
printf '%s' "$after_push" | grep -qE '([[:space:]]-f([[:space:]]|$)|--force([[:space:]]|$)|--force-with-lease)' && forced=1
[ "$forced" -eq 0 ] && return 0
if printf '%s' "$after_push" | grep -qE '(^|[[:space:]]|:)(main|master)([[:space:]]|:|$)'; then
block "force-push to main/master."
fi
if printf '%s' "$after_push" | grep -qE '[[:space:]]--all([[:space:]]|$)'; then
block "force-push --all rewrites every branch, main/master included."
fi
}
# --- 3: skipping safety checks -------------------------------------------
check_skip_safety() {
case "$command" in
*"--no-verify"*|*"--no-gpg-sign"*)
block "a flag that skips commit hooks or signature verification." ;;
esac
if printf '%s' "$command" | grep -qE 'chmod[[:space:]]+(-[a-zA-Z]+[[:space:]]+)*777'; then
block "chmod 777 — overly permissive file permissions."
fi
# Any remote fetch piped into any shell: curl|sh, curl|bash,
# wget|sh, wget | sudo bash, ...
if printf '%s' "$command" | grep -qE '(curl|wget)[^|]*\|[[:space:]]*(sudo[[:space:]]+)*(ba|z|k|da|c)?sh([[:space:]]|$)'; then
block "piping a remote script straight into a shell — read it first."
fi
}
# --- 4: secrets ----------------------------------------------------------
is_sensitive_path() {
case "$1" in
*".env"*|*"id_rsa"*|*"id_ed25519"*|*".aws/credentials"*|*".ssh/"*|*"credentials.json"*) return 0 ;;
*".git/config") return 0 ;;
esac
return 1
}
# Write/Edit/MultiEdit tools
check_sensitive_write() {
if is_sensitive_path "$file_path"; then
block "write to a sensitive/credentials path ($file_path). Edit it by hand instead."
fi
}
# The same files, but written from the shell — `echo ... >> .env`, `tee`.
# The file_path check above only sees the Write/Edit tools.
check_secret_write_via_bash() {
if printf '%s' "$command" | grep -qE '(>>?|tee[[:space:]]+(-a[[:space:]]+)*)[[:space:]]*[^[:space:]]*(\.env|id_rsa|id_ed25519|\.aws/credentials|credentials\.json|\.ssh/)'; then
block "writing to a secrets file from the shell. Edit it by hand instead."
fi
}
# --- dispatch ------------------------------------------------------------
case "$hook_event" in
PreToolUse)
if [ "$tool_name" = "Bash" ]; then
check_destructive_fs
check_protected_branch
check_skip_safety
check_secret_write_via_bash
fi
case "$tool_name" in
Write|Edit|MultiEdit) check_sensitive_write ;;
esac
;;
PostToolUse)
write_audit_log "RAN"
;;
esac
exit 0
Requires jq (already on most dev machines; brew install jq / apt install jq if not). Make it executable once: chmod +x .claude/guard.sh.
What this does not protect against
This is a guardrail against mistakes — yours and the agent's — not a security boundary. It is pattern matching on a command string, and pattern matching can always be walked around. Being straight about that is the point:
Deliberate evasion. Anything that hides the command from the matcher gets through: base64-decoding into a shell, writing a script and running it, an unusual alias, a language runtime shelling out.
Prompt injection. If a page or file talks the agent into something destructive, these checks only catch it when the final command happens to match a pattern.
Files pulled in with @. Attaching a file in the prompt doesn't call a tool, so PreToolUse never runs. The .env rule here does not stop .env being read that way — use a Read deny rule in permissions for that.
Everything not on the list. There is no sudo rule, no network rule, no package-install rule. Five checks, that's it.
Non-bash environments. It's a bash script with a jq dependency — on Windows it needs WSL or Git Bash.
Where it does earn its keep: the 2 a.m. rm -rf, the force-push to main you meant to aim at your branch, the curl | sh you didn't read, and a log of what actually happened.
Check that it's actually running
A hook that silently isn't wired up feels exactly like a hook that's working. Feed the script a payload by hand — no Claude Code required:
Then check registration with /hooks inside Claude Code, and run it with claude --debug to watch hooks fire. exit=0 where you expected 2 means the pattern didn't match; no output at all usually means the file isn't executable (chmod +x) or the path in settings.json is wrong.
Need to get past it on purpose? That's a fair question, and there's no clever answer: comment out the check, or move the hook into .claude/settings.local.json (git-ignored, yours only) instead of .claude/settings.json (committed, shared with the team). A guardrail you can't turn off is one people delete entirely.
These are the actual files, not a copy — the code blocks on this page are generated from them, so what you read above is byte-for-byte what you download.
Žingsnis 4 / 6
Hooks giliau: gyvenimo ciklas ir anatomija
Instrukcijos sako, ką agentas turėtų daryti. Hooks leidžia tavo sistemai reaguoti į tai, ką jis iš tikrųjų daro. Kur jie įsiterpia, iš ko sudarytas vienas hook ir kaip atrodo tas, kuris pasako „ne“, su interaktyviu pavyzdžiu.
(anglų k.)
You“Fix the login bug”
→
Claudedecides to edit .env
→
PreToolUse hook“Is this allowed?”
→
DeniedClaude is told .env is protected, and works around it
That is the entire concept. Everything below is detail on how each box works.
01 · The moment you need them
You ask Claude to fix a login bug. It reads the code, forms a theory, and decides the fastest fix is to change a value in .env. That is a reasonable engineering instinct. It is also the one file you never want an agent to touch, because it holds production credentials and it is not in version control, so a bad edit is not recoverable with git checkout.
You could write “never edit .env” in CLAUDE.md. That usually works. But it is an instruction competing with every other instruction in context, and it is advisory: nothing enforces it.
Instructions tell Claude what it should do. Hooks let your system react to what Claude actually does.
One is a request. The other is your code, running on your machine, at a fixed point in the agent's loop — with the power to say no.
02 · The analogy that makes it click
Think of Claude as a competent new employee in your office building, and hooks as the building's security system.
Claude
The employee. Capable, well-intentioned, allowed to move around and get work done.
the agent
Tools
The doors. Reading a file, writing a file, running a command — each one is a door into a room.
Read · Edit · Write · Bash
Hooks
Security and sensors. They sit at the doors, not inside the employee's head.
your scripts
From there the three hooks you will use most explain themselves:
PreToolUse is the badge reader outside the room. It checks before you go in, and it can refuse.
PostToolUse is the quality inspector who walks in afterwards. It cannot un-open the door, but it can tidy up, log what happened, or flag a problem.
Stop is the person at the exit asking “did you actually run the tests?” before you go home — and who can send you back in.
03 · Where hooks sit in a run
A Claude Code session is a loop, and hook events are named points along it. Here is the path a single tool call takes:
Session startsproject opens
SessionStart
You typea prompt
UserPromptSubmit
Claude thinkspicks a tool
PreToolUsecan deny
PermissionRequest
Tool runsfile written
PostToolUseor …Failure
Loopback to thinking
Stopcan send it back
Session endsSessionEnd
Scroll the row sideways if it is cut off. The important shape: PreToolUse is the only point that can stop an action before it happens. Everything after it is reacting to something that already ran.
Show the full lifecycle (33 events)
You do not need these to start, and memorising them is not the point. They exist so that almost anything the agent does can be observed or gated — subagents, tasks, compaction, model switches, config changes and worktrees all have their own events.
Every hook is the same three decisions, no matter how complex it gets:
HOOK = EVENT + MATCHER + ACTION
When should this fire · which calls do I care about · what do I run.
The event
The point in the loop where your code runs. PreToolUse means “before the tool executes”, which is the only place a decision can still change the outcome.
The matcher
A regular expression against the tool name, so the hook only wakes for calls it cares about. Edit|Write means file-writing tools and nothing else — a Read or a Bash call never reaches this script.
The handler
A command you own. Claude Code runs it and writes a JSON payload to its stdin, describing the call: tool_name, tool_input, cwd, session_id and more.
The result
Your script answers on stdout. Returning permissionDecision: "deny" blocks the call and hands your reason back to Claude as feedback. Printing nothing means “no opinion” and the normal permission flow continues.
Written out, that is this — and this shape is every hook you will ever write:
The smallest useful hook does not block anything. It runs Prettier over whatever file Claude just wrote, so formatting stops being something either of you thinks about.
The command reads the same JSON payload on stdin, pulls one field out of it with jq, and passes it to Prettier.
Why PostToolUse and not PreToolUse?
Because formatting is something you do to a file that exists. Before the tool runs, the change has not happened yet — there is nothing on disk to format. Pick the event by asking whether you need to influence the action or react to it.
06 · The one that says no
Now the .env problem from the top. This is a real hook, not pseudocode: it reads the payload on stdin, compares the target path against a protected list, and answers.
.claude/hooks/protect-files.mjs
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import path from "node:path";
const PROTECTED = [
/(^|\/)\.env(\..+)?$/, // .env, .env.local, .env.production
/(^|\/)\.git\//, // anything inside .git
/(^|\/)\.ssh\//, // keys
/(^|\/)credentials\.json$/,
];
// Claude Code writes the call description to stdin as JSON.
const payload = JSON.parse(readFileSync(0, "utf8"));
const target = payload.tool_input?.file_path ?? "";
const rel = path.relative(payload.cwd ?? process.cwd(), target) || target;
if (PROTECTED.some((re) => re.test(rel))) {
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason:
`${rel} is protected. Change it by hand, or ask a human.`,
},
}));
}
// Print nothing = no opinion. The normal permission flow continues.
process.exit(0);
Two things worth noticing. It says why, not just no — that reason goes back to Claude, which is the difference between it getting stuck and it finding another route. And silence is a valid answer: a hook that has no opinion should print nothing rather than approving everything.
Run something through it
Pick what Claude tries to do and watch the hook decide. The rules below are the ones in the script above.
Claude wants to:
07 · Hooks are not only about blocking
Blocking is the loudest use, not the most common one. Six things people actually wire up:
Observe
Write every tool call to a log so you can see afterwards what the agent actually did.
PostToolUse → append to a file
Automate
Run the formatter, regenerate types, restart a dev server after a file changes.
PostToolUse → prettier
Protect
Refuse edits to credentials, migrations, or anything with no undo.
PreToolUse → deny
Enrich
Inject context at the start of a session: the current branch, the ticket, today's deploy state.
SessionStart → stdout becomes context
Notify
Ping you when a long run finishes or the agent is waiting on a decision.
Notification · Stop
Verify
Refuse to let the session end while the tests are red, and say so.
Stop → continue: true
The Enrich row is worth a second look: on a few events — SessionStart, UserPromptSubmit among them — whatever your script prints on stdout is added to the context Claude can see. A three-line shell script can tell it which branch you are on.
08 · Rules versus judgement
A hook does not have to be a script. Alongside command, a hook can be a prompt or an agent — Claude evaluating the situation instead of your regex.
Deterministic
type: "command" — your code, same answer every time.
Fast and free
Testable like any other script
Cannot be talked out of a decision
Only as good as the pattern you wrote
Judgement
type: "prompt" or "agent" — a model weighs it up.
Handles cases you did not enumerate
Reads intent, not just strings
Costs tokens and time on every call
Not perfectly repeatable; agent hooks are experimental
Rule of thumb: if you can express the rule with an if, write the if. Reach for a prompt or agent hook only when the decision genuinely needs understanding — “is this commit message describing what actually changed?” is judgement; “is this file .env?” is not.
09 · What it looks like assembled
Put four small hooks together and the loop stops being a chat and starts being a workflow with checks in it:
SessionStartinjects the branch and ticket into context
Stoptests red? sends Claude back instead of finishing
None of those four is clever on its own. Together they mean the agent cannot touch your secrets, cannot leave the codebase badly formatted, cannot quietly finish on a broken build, and cannot claim it did not know which branch it was on. That is the whole pitch: same model, bounded system.
10 · A hook is code. Treat it like code.
A command hook runs as a shell command with your permissions. Not Claude's permissions, not a sandbox — yours. It can modify or delete anything your user account can reach, and it runs automatically, without asking, on every matching call.
Validate the input. The payload describes something the agent proposed; treat the strings in it as untrusted.
Quote your variables. An unquoted $VAR in a shell hook is a command-injection hole, and the value came from a model.
Guard against path traversal. Resolve to an absolute path before comparing, so ../../.env does not sail past a check that only looked at the basename.
Use absolute paths for the script itself, so behaviour does not depend on the working directory.
Do not read secrets in a hook just because it is convenient.
The failure mode to avoid is copying a .claude/settings.json from a random repository and letting it run. You would not curl | bash a stranger's script. A hook config is the same thing with extra steps: read it before it runs.
Hook event reference
The ones worth knowing by name, and what each is for.
Event
Fires
Use it to
SessionStart
session opens
inject context — stdout is added to what Claude sees
UserPromptSubmit
you send a prompt
add context, or reject the prompt
PreToolUse
before a tool runs
allow, deny, or rewrite the tool input
PermissionRequest
a call needs approval
auto-approve known-safe calls
PostToolUse
after a tool succeeds
format, log, regenerate
PostToolUseFailure
after a tool errors
react to failures, add a hint
Stop
Claude is about to finish
send it back if the work is not verified
SubagentStop
a subagent finishes
check a delegated result
Notification
Claude needs you
send yourself a desktop or phone ping
PreCompact
before context compaction
persist anything you do not want summarised away
SessionEnd
session closes
clean up, write a summary
Behaviour and event names verified against the official Claude Code hooks reference, September 2026. Hooks are evolving quickly — check the official reference before relying on a detail here.
Žingsnis 5 / 6
Savos komandos ir skills
Jei tą patį promptą rašai trečią kartą, tai jau ne promptas, o komanda. Kaip pasikartojantį darbą paversti vienu trumpiniu, kurį vienodai supranta ir agentas, ir tavo komanda.
(anglų k.)
The moment to write one
Onceyou type the prompt
→
Twiceyou retype it, slightly differently
→
Third timewrite the skill
The cost of a skill is about five minutes. The cost of not writing it is retyping a paragraph forever, slightly differently each time, and getting slightly different results because of it.
01 · Where they live
A skill is a folder with a SKILL.md inside. The folder name becomes the command name.
Location
Scope
Invoked as
~/.claude/skills/<name>/SKILL.md
you, every project
/<name>
.claude/skills/<name>/SKILL.md
this project, committed to git
/<name>
.claude/commands/<name>.md
legacy command file, still works
/<name>
Legacy command files namespace by subdirectory: .claude/commands/frontend/component.md becomes /frontend:component.
02 · What a SKILL.md looks like
YAML frontmatter, then plain markdown instructions. That is the whole format.
.claude/skills/review/SKILL.md
---
name: review
description: Review the current diff for bugs before I open a PR
argument-hint: [branch]
---
## Review the diff against $ARGUMENTS
1. Read the full diff, not just the changed lines
2. Flag anything that would fail CI
3. Check the tests actually cover the new branch of logic
4. Report findings worst-first, and stop
The fields worth knowing:
Field
What it does
description
when Claude should reach for this by itself — the one field you should always write
argument-hint
what shows in autocomplete after you type the command
allowed-tools
tools pre-approved for this turn, so it does not stop to ask
disable-model-invocation
only you can run it; Claude will not trigger it on its own
user-invocable
set false to hide it from the / menu and let only Claude use it
03 · Arguments
Everything typed after the command lands in $ARGUMENTS:
SKILL.md
Summarise these changes: $ARGUMENTS
/summarise add search to the header → Claude sees the whole sentence.
For more than one, use positions:
SKILL.md
---
arguments: [component, fromLang, toLang]
---
Migrate the $component component from $fromLang to $toLang.
Preserve behaviour and tests.
/migrate Button JavaScript TypeScript.
04 · The part that makes skills actually useful
A skill does not have to be static text. It can run commands and paste the real output into the prompt before Claude ever reads it.
SKILL.md
## Current state
!`git status --short`
!`git diff --stat`
## Instructions
Review only the files listed above.
And it can pull whole files in with @:
SKILL.md
## Conventions to follow
@./.eslintrc.json
## Code under review
@./src/main.js
This is the difference between a snippet and a tool.
A skill with !`git diff` in it is never stale. It describes how to look, and the looking happens fresh every time you run it.
05 · Pre-approving the tools it needs
A skill that stops four times to ask permission is not saving you anything. allowed-tools grants exactly what this command needs, for this turn only:
.claude/skills/commit/SKILL.md
---
name: commit
description: Stage and commit the current changes
disable-model-invocation: true
allowed-tools: Bash(git add *) Bash(git commit *) Bash(git status *)
---
Stage the changes, write a message describing why rather than what,
and commit. Do not push.
Note disable-model-invocation: true. Anything that writes, deploys or publishes should be something you trigger deliberately, not something the model can decide to run.
06 · A full one, end to end
~/.claude/skills/fix-issue/SKILL.md
---
name: fix-issue
description: Fix a GitHub issue by number. Use when asked to fix an issue.
argument-hint: [issue-number]
arguments: [issue]
allowed-tools: Bash(gh issue view *)
disable-model-invocation: true
---
## Fix issue #$issue
### The issue
!`gh issue view $issue --json title,body,comments`
### Steps
1. Understand what is actually being reported above
2. Find the relevant files yourself
3. Implement the fix in the smallest change that works
4. Run `npm test`
5. Commit as `fix(#$issue): <what changed>`
Run /fix-issue 123 and Claude starts with the real issue text already in front of it, rather than asking you to paste it.
Rules of thumb
Write the description for Claude, not for yourself — it is what decides whether the skill gets picked up automatically.
Keep SKILL.md short. It loads when invoked, so a long one costs context every time.
Ground it in live data with ! instead of describing the state in prose.
Verified against the official Claude Code documentation, September 2026. Claude Code moves quickly, so check the official documentation before relying on a detail here.
Žingsnis 6 / 6
MCP: kaip prijungti savo įrankius
Pagal nutylėjimą agentas mato tik failus. MCP yra būdas duoti jam tavo duomenų bazę, API ar vidinę sistemą, ir tiksliai tiek, kiek leidi, ne daugiau.
(anglų k.)
The signal that you need it
You keep copying things into the chat. A stack trace out of Sentry. A ticket out of Jira. A row out of the database. Every paste is context you had to fetch by hand, and it is stale the moment you paste it.
Without MCPyou look it up, copy it, paste it, and it is a snapshot
→
With MCPClaude queries the source itself, when it needs it
MCP (Model Context Protocol) is an open standard for connecting an agent to external tools and data. Claude Code speaks it, so anything with an MCP server — and most of the tools you already use have one — becomes something Claude can read and act on directly.
01 · Three kinds of server
stdio
Runs as a process on your machine. Good for local things: a database, a filesystem tool, something you wrote yourself.
--transport stdio
http
A remote service over HTTP. This is what most hosted tools give you.
--transport http
sse
The older remote transport. Deprecated, still supported.
--transport sse
02 · Adding one
A local server runs a command. Everything after -- is passed to that command untouched:
Expand them from the environment instead: ${API_KEY}, or ${API_BASE_URL:-https://api.example.com} to supply a default.
05 · Logging in
Most hosted servers use OAuth. Add the server, then authenticate from inside a session:
terminal
claude mcp add --transport http sentry https://mcp.sentry.dev/mcp
in the session
/mcp
That opens the browser login flow. From the shell, claude mcp login sentry does the same, and claude mcp logout sentry clears the credentials again.
06 · How the tools show up
Once connected, the server's tools appear under a predictable name:
naming
mcp__<server-name>__<tool-name>
# a `query` tool from a server named `database`
mcp__database__query
Which matters, because that name is what you write in permission rules, in a skill's allowed-tools, in a subagent's tool list, and in hook matchers. A matcher of mcp__database__.* catches everything that server exposes.
So the guardrails you already know apply here unchanged: an MCP tool is just another tool call, and a PreToolUse hook can deny it like any other.
07 · Managing them
terminal
claude mcp list # everything configured
claude mcp get notion # details for one
claude mcp remove notion # disconnect it
claude mcp add-from-claude-desktop # import what you already set up
/mcp inside a session shows live connection status, which is the first thing to check when a tool silently is not there.
08 · The part to be careful about
An MCP server decides what content enters Claude's context. That makes a server you do not control a genuine attack surface, not a theoretical one.
Trust the server before you connect it. A server that fetches external content can feed instructions into your session — this is prompt injection, and the agent has your permissions.
Project-scoped servers ask for approval before first use, because they arrive in a file someone else can commit.
headersHelper runs a shell command on your machine to produce auth headers. Read it before you approve it.
Scope narrowly. A read-only database server is a very different risk from one that can write.
The useful instinct: connecting an MCP server is closer to installing a dependency than to changing a setting. Apply the same suspicion you would to an unfamiliar npm package.
Verified against the official Claude Code documentation, September 2026. Claude Code moves quickly, so check the official documentation before relying on a detail here.